Initial import
This commit is contained in:
commit
54fa30701e
278 changed files with 37868 additions and 0 deletions
515
lib/models/campaigns.js
Normal file
515
lib/models/campaigns.js
Normal file
|
@ -0,0 +1,515 @@
|
|||
'use strict';
|
||||
|
||||
let tools = require('../tools');
|
||||
let db = require('../db');
|
||||
let lists = require('./lists');
|
||||
let templates = require('./templates');
|
||||
let segments = require('./segments');
|
||||
let shortid = require('shortid');
|
||||
|
||||
let allowedKeys = ['description', 'from', 'address', 'subject', 'template', 'list', 'segment', 'html', 'text'];
|
||||
|
||||
module.exports.list = (start, limit, callback) => {
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('SELECT SQL_CALC_FOUND_ROWS * FROM campaigns ORDER BY name LIMIT ? OFFSET ?', [limit, start], (err, rows) => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
connection.query('SELECT FOUND_ROWS() AS total', (err, total) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, rows, total && total[0] && total[0].total);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.getByCid = (cid, callback) => {
|
||||
cid = (cid || '').toString().trim();
|
||||
if (!cid) {
|
||||
return callback(new Error('Missing Campaign ID'));
|
||||
}
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('SELECT * FROM campaigns WHERE cid=?', [cid], (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!rows || !rows.length) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
let campaign = tools.convertKeys(rows[0]);
|
||||
return callback(null, campaign);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.get = (id, withSegment, callback) => {
|
||||
id = Number(id) || 0;
|
||||
|
||||
if (id < 1) {
|
||||
return callback(new Error('Missing Campaign ID'));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('SELECT * FROM campaigns WHERE id=?', [id], (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!rows || !rows.length) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
let campaign = tools.convertKeys(rows[0]);
|
||||
|
||||
if (!campaign.segment || !withSegment) {
|
||||
return callback(null, campaign);
|
||||
} else {
|
||||
segments.get(campaign.segment, (err, segment) => {
|
||||
if (err || !segment) {
|
||||
// ignore
|
||||
return callback(null, campaign);
|
||||
}
|
||||
segments.subscribers(segment.id, true, (err, subscribers) => {
|
||||
if (err || !subscribers) {
|
||||
segment.subscribers = 0;
|
||||
} else {
|
||||
segment.subscribers = subscribers;
|
||||
}
|
||||
campaign.segment = segment;
|
||||
return callback(null, campaign);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.create = (campaign, callback) => {
|
||||
|
||||
campaign = tools.convertKeys(campaign);
|
||||
let name = (campaign.name || '').toString().trim();
|
||||
|
||||
if (/^\d+:\d+$/.test(campaign.list)) {
|
||||
campaign.segment = Number(campaign.list.split(':').pop());
|
||||
campaign.list = Number(campaign.list.split(':').shift());
|
||||
} else {
|
||||
campaign.list = Number(campaign.list) || 0;
|
||||
campaign.segment = 0;
|
||||
}
|
||||
|
||||
campaign.template = Number(campaign.template) || 0;
|
||||
|
||||
if (!name) {
|
||||
return callback(new Error('Campaign Name must be set'));
|
||||
}
|
||||
|
||||
lists.get(campaign.list, (err, list) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (!list) {
|
||||
return callback(new Error('Selected list not found'));
|
||||
}
|
||||
|
||||
let keys = ['name'];
|
||||
let values = [name];
|
||||
|
||||
let create = () => {
|
||||
Object.keys(campaign).forEach(key => {
|
||||
let value = typeof campaign[key] === 'number' ? campaign[key] : (campaign[key] || '').toString().trim();
|
||||
key = tools.toDbKey(key);
|
||||
if (allowedKeys.indexOf(key) >= 0 && keys.indexOf(key) < 0) {
|
||||
keys.push(key);
|
||||
values.push(value);
|
||||
}
|
||||
});
|
||||
|
||||
let cid = shortid.generate();
|
||||
keys.push('cid');
|
||||
values.push(cid);
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let query = 'INSERT INTO campaigns (`' + keys.join('`, `') + '`) VALUES (' + values.map(() => '?').join(',') + ')';
|
||||
connection.query(query, values, (err, result) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let campaignId = result && result.insertId || false;
|
||||
if (!campaignId) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
// we are going to aqcuire a lot of log info, so we are putting
|
||||
// sending logs into separate tables
|
||||
createCampaignTables(campaignId, err => {
|
||||
if (err) {
|
||||
// FIXME: rollback
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, campaignId);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if (campaign.template) {
|
||||
templates.get(campaign.template, (err, template) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (!template) {
|
||||
return callback(new Error('Selected template not found'));
|
||||
}
|
||||
|
||||
keys = keys.concat(['html', 'text']);
|
||||
values = values.concat([template.html, template.text]);
|
||||
|
||||
create();
|
||||
});
|
||||
} else {
|
||||
create();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.update = (id, updates, callback) => {
|
||||
updates = updates || {};
|
||||
id = Number(id) || 0;
|
||||
|
||||
if (id < 1) {
|
||||
return callback(new Error('Missing Campaign ID'));
|
||||
}
|
||||
|
||||
let campaign = tools.convertKeys(updates);
|
||||
let name = (campaign.name || '').toString().trim();
|
||||
|
||||
if (!name) {
|
||||
return callback(new Error('Campaign Name must be set'));
|
||||
}
|
||||
|
||||
if (/^\d+:\d+$/.test(campaign.list)) {
|
||||
campaign.segment = Number(campaign.list.split(':').pop());
|
||||
campaign.list = Number(campaign.list.split(':').shift());
|
||||
} else {
|
||||
campaign.list = Number(campaign.list) || 0;
|
||||
campaign.segment = 0;
|
||||
}
|
||||
|
||||
lists.get(campaign.list, (err, list) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (!list) {
|
||||
return callback(new Error('Selected list not found'));
|
||||
}
|
||||
|
||||
let keys = ['name'];
|
||||
let values = [name];
|
||||
|
||||
Object.keys(campaign).forEach(key => {
|
||||
let value = typeof campaign[key] === 'number' ? campaign[key] : (campaign[key] || '').toString().trim();
|
||||
key = tools.toDbKey(key);
|
||||
if (allowedKeys.indexOf(key) >= 0 && keys.indexOf(key) < 0) {
|
||||
keys.push(key);
|
||||
values.push(value);
|
||||
}
|
||||
});
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
values.push(id);
|
||||
|
||||
connection.query('UPDATE campaigns SET ' + keys.map(key => '`' + key + '`=?').join(', ') + ' WHERE id=? LIMIT 1', values, (err, result) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, result && result.affectedRows || false);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.delete = (id, callback) => {
|
||||
id = Number(id) || 0;
|
||||
|
||||
if (id < 1) {
|
||||
return callback(new Error('Missing Campaign ID'));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('DELETE FROM campaigns WHERE id=? LIMIT 1', [id], (err, result) => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let affected = result && result.affectedRows || 0;
|
||||
|
||||
connection.query('DELETE FROM links WHERE campaign=?', [id], err => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
removeCampaignTables(id, err => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, affected);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.send = (id, callback) => {
|
||||
module.exports.get(id, false, (err, campaign) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (campaign.status === 2) { // already sending
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// campaigns marked as status=2 should be picked up by the sending processes
|
||||
connection.query('UPDATE campaigns SET `status`=2, `status_change`=NOW() WHERE id=? LIMIT 1', [id], err => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.pause = (id, callback) => {
|
||||
module.exports.get(id, false, (err, campaign) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (campaign.status !== 2) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// campaigns marked as status=4 are paused
|
||||
connection.query('UPDATE campaigns SET `status`=4, `status_change`=NOW() WHERE id=? LIMIT 1', [id], err => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.reset = (id, callback) => {
|
||||
module.exports.get(id, false, (err, campaign) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (campaign.status !== 3) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('UPDATE campaigns SET `status`=1, `status_change`=NULL, `delivered`=0, `opened`=0, `clicks`=0, `bounced`=0, `complained`=0, `unsubscribed`=0 WHERE id=? LIMIT 1', [id], err => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
connection.query('DELETE FROM links WHERE campaign=?', [id], err => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
connection.query('TRUNCATE TABLE `campaign__' + id + '`', [id], err => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
connection.query('TRUNCATE TABLE `campaign_tracker__' + id + '`', [id], err => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.getMail = (campaignId, listId, subscriptionId, callback) => {
|
||||
campaignId = Number(campaignId) || 0;
|
||||
listId = Number(listId) || 0;
|
||||
subscriptionId = Number(subscriptionId) || 0;
|
||||
|
||||
if (campaignId < 1 || listId < 1 || subscriptionId < 1) {
|
||||
return callback(new Error('Invalid or missing message ID'));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('SELECT * FROM `campaign__' + campaignId + '` WHERE list=? AND subscription=?', [listId, subscriptionId], (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!rows || !rows.length) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
let campaign = tools.convertKeys(rows[0]);
|
||||
return callback(null, campaign);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.findMail = (responseId, callback) => {
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('SELECT id FROM campaigns', [], (err, campaignList) => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
if (!campaignList || !campaignList.length) {
|
||||
connection.release();
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
let pos = 0;
|
||||
let checkNext = () => {
|
||||
if (pos >= campaignList.length) {
|
||||
// all campaigns checked, result not found
|
||||
connection.release();
|
||||
return callback(null, false);
|
||||
}
|
||||
let campaign = campaignList[pos++];
|
||||
|
||||
connection.query('SELECT id, list, segment, subscription FROM `campaign__' + campaign.id + '` WHERE `response_id`=? LIMIT 1', [responseId], (err, rows) => {
|
||||
if (err || !rows || !rows.length) {
|
||||
return checkNext();
|
||||
}
|
||||
|
||||
let message = rows[0];
|
||||
message.campaign = campaign.id;
|
||||
connection.release();
|
||||
|
||||
return callback(null, message);
|
||||
});
|
||||
};
|
||||
|
||||
checkNext();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function createCampaignTables(id, callback) {
|
||||
let query = 'CREATE TABLE `campaign__' + id + '` LIKE campaign';
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
connection.query(query, err => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
let query = 'CREATE TABLE `campaign_tracker__' + id + '` LIKE campaign_tracker';
|
||||
connection.query(query, err => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function removeCampaignTables(id, callback) {
|
||||
let query = 'DROP TABLE IF EXISTS `campaign__' + id + '`';
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
connection.query(query, err => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
let query = 'DROP TABLE IF EXISTS `campaign_tracker__' + id + '`';
|
||||
connection.query(query, err => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
503
lib/models/fields.js
Normal file
503
lib/models/fields.js
Normal file
|
@ -0,0 +1,503 @@
|
|||
'use strict';
|
||||
|
||||
let db = require('../db');
|
||||
let tools = require('../tools');
|
||||
let slugify = require('slugify');
|
||||
let lists = require('./lists');
|
||||
let shortid = require('shortid');
|
||||
|
||||
let allowedKeys = ['name', 'key', 'default_value', 'group', 'visible'];
|
||||
|
||||
module.exports.grouped = ['radio', 'checkbox', 'dropdown'];
|
||||
module.exports.types = {
|
||||
text: 'Text',
|
||||
website: 'Website',
|
||||
number: 'Number',
|
||||
radio: 'Radio Buttons',
|
||||
checkbox: 'Checkboxes',
|
||||
dropdown: 'Drop Down',
|
||||
'date-us': 'Date (MM/DD/YYY)',
|
||||
'date-eur': 'Date (DD/MM/YYYY)',
|
||||
'birthday-us': 'Birthday (MM/DD)',
|
||||
'birthday-eur': 'Birthday (DD/MM)',
|
||||
option: 'Option'
|
||||
};
|
||||
|
||||
module.exports.genericTypes = {
|
||||
text: 'string',
|
||||
website: 'string',
|
||||
number: 'number',
|
||||
'date-us': 'date',
|
||||
'date-eur': 'date',
|
||||
'birthday-us': 'birthday',
|
||||
'birthday-eur': 'birthday',
|
||||
option: 'boolean'
|
||||
};
|
||||
|
||||
module.exports.list = (listId, callback) => {
|
||||
listId = Number(listId) || 0;
|
||||
|
||||
if (listId < 1) {
|
||||
return callback(new Error('Missing List ID'));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let query = 'SELECT * FROM custom_fields WHERE list=?';
|
||||
connection.query(query, [listId], (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let fieldList = rows && rows.map(row => tools.convertKeys(row)) || [];
|
||||
let groups = new Map();
|
||||
|
||||
// remove grouped rows
|
||||
for (let i = fieldList.length - 1; i >= 0; i--) {
|
||||
let field = fieldList[i];
|
||||
if (module.exports.grouped.indexOf(field.type) >= 0) {
|
||||
if (!groups.has(field.id)) {
|
||||
groups.set(field.id, []);
|
||||
}
|
||||
field.options = groups.get(field.id);
|
||||
} else if (field.group && field.type === 'option') {
|
||||
if (!groups.has(field.group)) {
|
||||
groups.set(field.group, [field]);
|
||||
} else {
|
||||
groups.get(field.group).unshift(field);
|
||||
}
|
||||
fieldList.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
return callback(null, fieldList);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.get = (id, callback) => {
|
||||
id = Number(id) || 0;
|
||||
|
||||
if (id < 1) {
|
||||
return callback(new Error('Missing List ID'));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let query = 'SELECT * FROM custom_fields WHERE id=? LIMIT 1';
|
||||
connection.query(query, [id], (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, rows && rows[0] && tools.convertKeys(rows[0]) || false);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.create = (listId, field, callback) => {
|
||||
listId = Number(listId) || 0;
|
||||
|
||||
if (listId < 1) {
|
||||
return callback(new Error('Missing List ID'));
|
||||
}
|
||||
|
||||
field = tools.convertKeys(field);
|
||||
|
||||
if (field.type === 'option' && !field.group) {
|
||||
return callback(new Error('Option field requires a group to be selected'));
|
||||
}
|
||||
|
||||
if (field.type !== 'option') {
|
||||
field.group = null;
|
||||
}
|
||||
|
||||
addCustomField(listId, field.name, field.defaultValue, field.type, field.group, field.visible, callback);
|
||||
};
|
||||
|
||||
module.exports.update = (id, updates, callback) => {
|
||||
updates = updates || {};
|
||||
id = Number(id) || 0;
|
||||
|
||||
updates = tools.convertKeys(updates);
|
||||
|
||||
if (id < 1) {
|
||||
return callback(new Error('Missing Field ID'));
|
||||
}
|
||||
|
||||
if (!(updates.name || '').toString().trim()) {
|
||||
return callback(new Error('Field Name must be set'));
|
||||
}
|
||||
|
||||
if (updates.key) {
|
||||
updates.key = slugify(updates.key, '_').toUpperCase();
|
||||
}
|
||||
|
||||
updates.visible = updates.visible ? 1 : 0;
|
||||
|
||||
let name = (updates.name || '').toString().trim();
|
||||
let keys = ['name'];
|
||||
let values = [name];
|
||||
|
||||
Object.keys(updates).forEach(key => {
|
||||
let value = typeof updates[key] === 'string' ? updates[key].trim() : updates[key];
|
||||
key = tools.toDbKey(key);
|
||||
if (allowedKeys.indexOf(key) >= 0) {
|
||||
keys.push(key);
|
||||
values.push(value);
|
||||
}
|
||||
});
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
values.push(id);
|
||||
|
||||
connection.query('UPDATE custom_fields SET ' + keys.map(key => '`' + key + '`=?').join(', ') + ' WHERE id=? LIMIT 1', values, (err, result) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, result && result.affectedRows || false);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.delete = (fieldId, callback) => {
|
||||
fieldId = Number(fieldId) || 0;
|
||||
|
||||
if (fieldId < 1) {
|
||||
return callback(new Error('Missing Field ID'));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let query = 'SELECT * FROM custom_fields WHERE id=? LIMIT 1';
|
||||
connection.query(query, [fieldId], (err, rows) => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!rows || !rows.length) {
|
||||
connection.release();
|
||||
return callback(new Error('Custom field not found'));
|
||||
}
|
||||
|
||||
let field = tools.convertKeys(rows[0]);
|
||||
if (field.column) {
|
||||
connection.query('ALTER TABLE `subscription__' + field.list + '` DROP COLUMN `' + field.column + '`', err => {
|
||||
if (err && err.code !== 'ER_CANT_DROP_FIELD_OR_KEY') {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
connection.query('DELETE FROM custom_fields WHERE id=? LIMIT 1', [fieldId], err => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
connection.query('DELETE FROM segment_rules WHERE column=? LIMIT 1', [field.column], err => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
// ignore
|
||||
}
|
||||
return callback(null, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// delete all subfields in this group
|
||||
let query = 'SELECT id FROM custom_fields WHERE `group`=?';
|
||||
connection.query(query, [fieldId], (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!rows || !rows.length) {
|
||||
rows = [];
|
||||
}
|
||||
|
||||
let pos = 0;
|
||||
let deleteNext = () => {
|
||||
if (pos >= rows.length) {
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
connection.query('DELETE FROM custom_fields WHERE id=? LIMIT 1', [fieldId], err => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, true);
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
module.exports.delete(rows[pos++].id, deleteNext);
|
||||
};
|
||||
|
||||
deleteNext();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function addCustomField(listId, name, defaultValue, type, group, visible, callback) {
|
||||
type = (type || '').toString().trim().toLowerCase();
|
||||
group = Number(group) || null;
|
||||
listId = Number(listId) || 0;
|
||||
|
||||
let column = null;
|
||||
let key = slugify('merge ' + name, '_').toUpperCase();
|
||||
let allowedTypes = ['text', 'number', 'radio', 'checkbox', 'dropdown', 'date-us', 'date-eur', 'birthday-us', 'birthday-eur', 'website', 'option'];
|
||||
|
||||
if (allowedTypes.indexOf(type) < 0) {
|
||||
return callback(new Error('Unknown column type ' + type));
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
return callback(new Error('Missing column name'));
|
||||
}
|
||||
|
||||
if (listId <= 0) {
|
||||
return callback(new Error('Missing list ID'));
|
||||
}
|
||||
|
||||
lists.get(listId, (err, list) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (!list) {
|
||||
return callback('Provided List ID not found');
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (module.exports.grouped.indexOf(type) < 0) {
|
||||
column = ('custom_' + slugify(name, '_') + '_' + shortid.generate()).toLowerCase().replace(/[^a-z0-9\_]/g, '');
|
||||
}
|
||||
|
||||
let query = 'INSERT INTO custom_fields (`list`, `name`, `key`,`default_value`, `type`, `group`, `column`, `visible`) VALUES(?,?,?,?,?,?,?,?)';
|
||||
connection.query(query, [listId, name, key, defaultValue, type, group, column, visible ? 1 : 0], (err, result) => {
|
||||
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let fieldId = result && result.insertId;
|
||||
|
||||
switch (type) {
|
||||
case 'text':
|
||||
case 'website':
|
||||
query = 'ALTER TABLE `subscription__' + listId + '` ADD COLUMN `' + column + '` VARCHAR(255) DEFAULT NULL';
|
||||
break;
|
||||
case 'number':
|
||||
query = 'ALTER TABLE `subscription__' + listId + '` ADD COLUMN `' + column + '` INT(11) DEFAULT NULL, ADD INDEX (' + column + ')';
|
||||
break;
|
||||
case 'option':
|
||||
query = 'ALTER TABLE `subscription__' + listId + '` ADD COLUMN `' + column + '` TINYINT(4) UNSIGNED NOT NULL DEFAULT \'0\', ADD INDEX (' + column + ')';
|
||||
break;
|
||||
case 'date-us':
|
||||
case 'date-eur':
|
||||
case 'birthday-us':
|
||||
case 'birthday-eur':
|
||||
query = 'ALTER TABLE `subscription__' + listId + '` ADD COLUMN `' + column + '` TIMESTAMP NULL DEFAULT NULL, ADD INDEX (' + column + ')';
|
||||
break;
|
||||
default:
|
||||
connection.release();
|
||||
return callback(null, true);
|
||||
}
|
||||
|
||||
connection.query(query, err => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
connection.query('DELETE FROM custom_fields WHERE id=? LIMIT 1', [fieldId], () => false);
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, fieldId);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports.getRow = (fieldList, values, useDate, showAll) => {
|
||||
let valueList = {};
|
||||
let row = [];
|
||||
|
||||
Object.keys(values || {}).forEach(key => {
|
||||
let value = values[key];
|
||||
key = tools.toDbKey(key);
|
||||
if (key.indexOf('custom_') === 0) {
|
||||
valueList[key] = value;
|
||||
} else if (key.indexOf('group_g') === 0 && value.indexOf('custom_') === 0) {
|
||||
valueList[tools.toDbKey(value)] = 1;
|
||||
}
|
||||
});
|
||||
|
||||
fieldList.filter(field => showAll || field.visible).forEach(field => {
|
||||
switch (field.type) {
|
||||
case 'text':
|
||||
case 'website':
|
||||
{
|
||||
let item = {
|
||||
type: field.type,
|
||||
name: field.name,
|
||||
column: field.column,
|
||||
value: (valueList[field.column] || '').toString().trim(),
|
||||
visible: !!field.visible,
|
||||
mergeTag: field.key,
|
||||
mergeValue: (valueList[field.column] || '').toString().trim() || field.defaultValue,
|
||||
['type' + (field.type || '').toString().trim().replace(/(?:^|\-)([a-z])/g, (m, c) => c.toUpperCase())]: true
|
||||
};
|
||||
row.push(item);
|
||||
break;
|
||||
}
|
||||
case 'number':
|
||||
{
|
||||
let item = {
|
||||
type: field.type,
|
||||
name: field.name,
|
||||
column: field.column,
|
||||
value: Number(valueList[field.column]) || 0,
|
||||
visible: !!field.visible,
|
||||
mergeTag: field.key,
|
||||
mergeValue: Number(valueList[field.column]) || field.defaultValue || 0,
|
||||
['type' + (field.type || '').toString().trim().replace(/(?:^|\-)([a-z])/g, (m, c) => c.toUpperCase())]: true
|
||||
};
|
||||
row.push(item);
|
||||
break;
|
||||
}
|
||||
case 'dropdown':
|
||||
case 'radio':
|
||||
case 'checkbox':
|
||||
{
|
||||
let item = {
|
||||
type: field.type,
|
||||
name: field.name,
|
||||
visible: !!field.visible,
|
||||
key: 'group-g' + field.id,
|
||||
mergeTag: field.key,
|
||||
mergeValue: field.defaultValue,
|
||||
['type' + (field.type || '').toString().trim().replace(/(?:^|\-)([a-z])/g, (m, c) => c.toUpperCase())]: true,
|
||||
options: (field.options || []).map(subField => ({
|
||||
type: subField.type,
|
||||
name: subField.name,
|
||||
column: subField.column,
|
||||
value: valueList[subField.column] ? 1 : 0,
|
||||
visible: !!subField.visible,
|
||||
mergeTag: subField.key,
|
||||
mergeValue: valueList[subField.column] ? subField.name : subField.defaultValue
|
||||
}))
|
||||
};
|
||||
item.value = item.options.filter(subField => showAll || subField.visible && subField.value).map(subField => subField.name).join(', ');
|
||||
item.mergeValue = item.value || field.defaultValue;
|
||||
row.push(item);
|
||||
break;
|
||||
}
|
||||
case 'date-eur':
|
||||
case 'birthday-eur':
|
||||
case 'date-us':
|
||||
case 'birthday-us':
|
||||
{
|
||||
let isUs = /\-us$/.test(field.type);
|
||||
let isYear = field.type.indexOf('date-') === 0;
|
||||
let value = valueList[field.column];
|
||||
let day, month, year;
|
||||
let formatted;
|
||||
|
||||
if (value && typeof value.getUTCFullYear === 'function') {
|
||||
day = value.getUTCDate();
|
||||
month = value.getUTCMonth() + 1;
|
||||
year = value.getUTCFullYear();
|
||||
} else {
|
||||
value = (value || '').toString().trim();
|
||||
|
||||
let parts = value.match(/(\d+)\D+(\d+)(?:\D+(\d+))?/);
|
||||
if (!parts) {
|
||||
value = null;
|
||||
} else {
|
||||
day = Number(parts[isUs ? 2 : 1]) || 0;
|
||||
month = Number(parts[isUs ? 1 : 2]) || 0;
|
||||
year = Number(parts[3]) || 2000;
|
||||
|
||||
if (!day || !month) {
|
||||
value = null;
|
||||
} else {
|
||||
value = new Date(Date.UTC(year, month - 1, day));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (day && month) {
|
||||
if (isUs) {
|
||||
formatted = (month < 10 ? '0' : '') + month + '/' + (day < 10 ? '0' : '') + day;
|
||||
} else {
|
||||
formatted = (day < 10 ? '0' : '') + day + '/' + (month < 10 ? '0' : '') + month;
|
||||
}
|
||||
|
||||
if (isYear) {
|
||||
formatted += '/' + year;
|
||||
}
|
||||
} else {
|
||||
formatted = null;
|
||||
}
|
||||
|
||||
let item = {
|
||||
type: field.type,
|
||||
name: field.name,
|
||||
column: field.column,
|
||||
value: useDate ? value : formatted,
|
||||
visible: !!field.visible,
|
||||
mergeTag: field.key,
|
||||
mergeValue: (useDate ? value : formatted) || field.defaultValue,
|
||||
['type' + (field.type || '').toString().trim().replace(/(?:^|\-)([a-z])/g, (m, c) => c.toUpperCase())]: true
|
||||
};
|
||||
row.push(item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return row;
|
||||
};
|
||||
|
||||
module.exports.getValues = (row, showAll) => {
|
||||
let result = [];
|
||||
row.filter(field => showAll || field.visible).forEach(field => {
|
||||
if (field.column) {
|
||||
result.push({
|
||||
key: field.column,
|
||||
value: field.value
|
||||
});
|
||||
} else if (field.options) {
|
||||
field.options.filter(field => showAll || field.visible).forEach(subField => {
|
||||
result.push({
|
||||
key: subField.column,
|
||||
value: subField.value
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
344
lib/models/links.js
Normal file
344
lib/models/links.js
Normal file
|
@ -0,0 +1,344 @@
|
|||
'use strict';
|
||||
|
||||
let db = require('../db');
|
||||
let shortid = require('shortid');
|
||||
let util = require('util');
|
||||
|
||||
let geoip = require('geoip-ultralight');
|
||||
let campaigns = require('./campaigns');
|
||||
let subscriptions = require('./subscriptions');
|
||||
let lists = require('./lists');
|
||||
|
||||
let log = require('npmlog');
|
||||
let urllib = require('url');
|
||||
|
||||
module.exports.resolve = (campaignCid, linkCid, callback) => {
|
||||
campaigns.getByCid(campaignCid, (err, campaign) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (!campaign) {
|
||||
return callback('Campaign not found');
|
||||
}
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
let query = 'SELECT id, url FROM links WHERE `campaign`=? AND `cid`=? LIMIT 1';
|
||||
connection.query(query, [campaign.id, linkCid], (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (rows && rows.length) {
|
||||
return callback(null, rows[0].id, rows[0].url);
|
||||
}
|
||||
|
||||
return callback(null, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.countClick = (remoteIp, campaignCid, listCid, subscriptionCid, linkId, callback) => {
|
||||
getSubscriptionData(campaignCid, listCid, subscriptionCid, (err, data) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.beginTransaction(err => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let country = geoip.lookupCountry(remoteIp) || null;
|
||||
|
||||
let query = 'INSERT INTO `campaign_tracker__' + data.campaign.id + '` (`list`, `subscriber`, `link`, `ip`, `country`) VALUES (?,?,?,?,?) ON DUPLICATE KEY UPDATE `count`=`count`+1';
|
||||
connection.query(query, [data.list.id, data.subscription.id, linkId, remoteIp, country], (err, result) => {
|
||||
if (err && err.code !== 'ER_DUP_ENTRY') {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
|
||||
if (err && err.code === 'ER_DUP_ENTRY' || result.affectedRows > 1) {
|
||||
return connection.commit(err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
connection.release();
|
||||
return callback(null, false);
|
||||
});
|
||||
}
|
||||
|
||||
let query = 'UPDATE `subscription__' + data.list.id + '` SET `latest_click`=NOW(), `latest_open`=NOW() WHERE id=?';
|
||||
connection.query(query, [data.subscription.id], err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
|
||||
let query = 'UPDATE links SET clicks = clicks + 1 WHERE id=?';
|
||||
connection.query(query, [linkId], err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
|
||||
let query = 'INSERT INTO `campaign_tracker__' + data.campaign.id + '` (`list`, `subscriber`, `link`, `ip`, `country`) VALUES (?,?,?,?,?)';
|
||||
connection.query(query, [data.list.id, data.subscription.id, 0, remoteIp, country], err => {
|
||||
if (err && err.code !== 'ER_DUP_ENTRY') {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
|
||||
if (err && err.code === 'ER_DUP_ENTRY') {
|
||||
return connection.commit(err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
connection.release();
|
||||
return callback(null, false);
|
||||
});
|
||||
}
|
||||
|
||||
let query = 'UPDATE campaigns SET clicks = clicks + 1 WHERE id=?';
|
||||
connection.query(query, [data.campaign.id], err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
connection.commit(err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
connection.release();
|
||||
return callback(null, false);
|
||||
});
|
||||
});
|
||||
|
||||
// also count clicks as open events in case beacon image was blocked
|
||||
module.exports.countOpen(remoteIp, campaignCid, listCid, subscriptionCid, () => false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.countOpen = (remoteIp, campaignCid, listCid, subscriptionCid, callback) => {
|
||||
getSubscriptionData(campaignCid, listCid, subscriptionCid, (err, data) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.beginTransaction(err => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let country = geoip.lookupCountry(remoteIp) || null;
|
||||
|
||||
let query = 'INSERT INTO `campaign_tracker__' + data.campaign.id + '` (`list`, `subscriber`, `link`, `ip`, `country`) VALUES (?,?,?,?,?) ON DUPLICATE KEY UPDATE `count`=`count`+1';
|
||||
connection.query(query, [data.list.id, data.subscription.id, -1, remoteIp, country], (err, result) => {
|
||||
if (err && err.code !== 'ER_DUP_ENTRY') {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
if (err && err.code === 'ER_DUP_ENTRY' || result.affectedRows > 1) {
|
||||
return connection.commit(err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
connection.release();
|
||||
return callback(null, false);
|
||||
});
|
||||
}
|
||||
|
||||
let query = 'UPDATE `subscription__' + data.list.id + '` SET `latest_open`=NOW() WHERE id=?';
|
||||
connection.query(query, [data.subscription.id], err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
|
||||
let query = 'UPDATE campaigns SET opened = opened + 1 WHERE id=?';
|
||||
connection.query(query, [data.campaign.id], err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
connection.commit(err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
connection.release();
|
||||
return callback(null, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.add = (url, campaignId, callback) => {
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let cid = shortid.generate();
|
||||
let query = 'INSERT INTO links (`cid`, `campaign`, `url`) VALUES (?,?,?)';
|
||||
connection.query(query, [cid, campaignId, url], (err, result) => {
|
||||
if (err && err.code !== 'ER_DUP_ENTRY') {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!err && result && result.insertId) {
|
||||
connection.release();
|
||||
return callback(null, result.insertId, cid);
|
||||
}
|
||||
|
||||
let query = 'SELECT id, cid FROM links WHERE `campaign`=? AND `url`=? LIMIT 1';
|
||||
connection.query(query, [campaignId, url], (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (rows && rows.length) {
|
||||
return callback(null, rows[0].id, rows[0].cid);
|
||||
}
|
||||
|
||||
return callback(null, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.updateLinks = (campaign, list, subscription, serviceUrl, message, callback) => {
|
||||
let re = /(<a[^>]* href\s*=[\s"']*)(http[^"'>\s]+)/gi;
|
||||
let urls = new Set();
|
||||
message.replace(re, (match, prefix, url) => {
|
||||
urls.add(url);
|
||||
});
|
||||
|
||||
let map = new Map();
|
||||
let vals = urls.values();
|
||||
|
||||
// insert tracking image
|
||||
let inserted = false;
|
||||
let imgUrl = urllib.resolve(serviceUrl, util.format('/links/%s/%s/%s', campaign.cid, list.cid, encodeURIComponent(subscription.cid)));
|
||||
let img = '<img src="' + imgUrl + '" width="1" height="1">';
|
||||
message = message.replace(/<\/body\b/i, match => {
|
||||
inserted = true;
|
||||
return img + match;
|
||||
});
|
||||
if (!inserted) {
|
||||
message = message + img;
|
||||
}
|
||||
|
||||
let replaceUrls = () => {
|
||||
callback(null,
|
||||
message.replace(re, (match, prefix, url) =>
|
||||
prefix + (map.has(url) ? urllib.resolve(serviceUrl, util.format('/links/%s/%s/%s/%s', campaign.cid, list.cid, encodeURIComponent(subscription.cid), encodeURIComponent(map.get(url)))) : url)));
|
||||
};
|
||||
|
||||
let storeNext = () => {
|
||||
let urlItem = vals.next();
|
||||
if (urlItem.done) {
|
||||
return replaceUrls();
|
||||
}
|
||||
|
||||
module.exports.add(urlItem.value, campaign.id, (err, linkId, cid) => {
|
||||
if (err) {
|
||||
log.error('Link', err.stack);
|
||||
return storeNext();
|
||||
}
|
||||
map.set(urlItem.value, cid);
|
||||
return storeNext();
|
||||
});
|
||||
};
|
||||
|
||||
storeNext();
|
||||
};
|
||||
|
||||
function getSubscriptionData(campaignCid, listCid, subscriptionCid, callback) {
|
||||
campaigns.getByCid(campaignCid, (err, campaign) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (!campaign) {
|
||||
return callback(new Error('Campaign not found'));
|
||||
}
|
||||
|
||||
lists.getByCid(listCid, (err, list) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (!list) {
|
||||
return callback(new Error('Campaign not found'));
|
||||
}
|
||||
|
||||
subscriptions.get(list.id, subscriptionCid, (err, subscription) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (!subscription) {
|
||||
return callback(new Error('Subscription not found'));
|
||||
}
|
||||
|
||||
return callback(null, {
|
||||
campaign,
|
||||
list,
|
||||
subscription
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
289
lib/models/lists.js
Normal file
289
lib/models/lists.js
Normal file
|
@ -0,0 +1,289 @@
|
|||
'use strict';
|
||||
|
||||
let db = require('../db');
|
||||
let tools = require('../tools');
|
||||
let shortid = require('shortid');
|
||||
let segments = require('./segments');
|
||||
|
||||
let allowedKeys = ['description'];
|
||||
|
||||
module.exports.list = (start, limit, callback) => {
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('SELECT SQL_CALC_FOUND_ROWS * FROM lists ORDER BY name LIMIT ? OFFSET ?', [limit, start], (err, rows) => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
connection.query('SELECT FOUND_ROWS() AS total', (err, total) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, rows, total && total[0] && total[0].total);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.quicklist = callback => {
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('SELECT id, name, subscribers FROM lists ORDER BY name LIMIT 1000', (err, rows) => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let lists = (rows || []).map(tools.convertKeys);
|
||||
|
||||
connection.query('SELECT id, list, name FROM segments ORDER BY list, name LIMIT 1000', (err, rows) => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let segments = (rows || []).map(tools.convertKeys);
|
||||
|
||||
lists.forEach(list => {
|
||||
list.segments = segments.filter(segment => segment.list === list.id);
|
||||
});
|
||||
|
||||
connection.release();
|
||||
return callback(null, lists);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.getByCid = (cid, callback) => {
|
||||
resolveCid(cid, (err, id) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (!id) {
|
||||
return callback(null, false);
|
||||
}
|
||||
module.exports.get(id, callback);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.get = (id, callback) => {
|
||||
id = Number(id) || 0;
|
||||
|
||||
if (id < 1) {
|
||||
return callback(new Error('Missing List ID'));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('SELECT * FROM lists WHERE id=?', [id], (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!rows || !rows.length) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
let list = tools.convertKeys(rows[0]);
|
||||
segments.list(list.id, (err, segmentList) => {
|
||||
if (err || !segmentList) {
|
||||
segmentList = [];
|
||||
}
|
||||
list.segments = segmentList;
|
||||
return callback(null, list);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.create = (list, callback) => {
|
||||
|
||||
let data = tools.convertKeys(list);
|
||||
let name = (data.name || '').toString().trim();
|
||||
|
||||
if (!data) {
|
||||
return callback(new Error('List Name must be set'));
|
||||
}
|
||||
|
||||
let keys = ['name'];
|
||||
let values = [name];
|
||||
|
||||
Object.keys(list).forEach(key => {
|
||||
let value = list[key].trim();
|
||||
key = tools.toDbKey(key);
|
||||
if (allowedKeys.indexOf(key) >= 0) {
|
||||
keys.push(key);
|
||||
values.push(value);
|
||||
}
|
||||
});
|
||||
|
||||
let cid = shortid.generate();
|
||||
keys.push('cid');
|
||||
values.push(cid);
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let query = 'INSERT INTO lists (`' + keys.join('`, `') + '`) VALUES (' + values.map(() => '?').join(',') + ')';
|
||||
connection.query(query, values, (err, result) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let listId = result && result.insertId || false;
|
||||
if (!listId) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
createSubscriptionTable(listId, err => {
|
||||
if (err) {
|
||||
// FIXME: rollback
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, listId);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.update = (id, updates, callback) => {
|
||||
updates = updates || {};
|
||||
id = Number(id) || 0;
|
||||
|
||||
let data = tools.convertKeys(updates);
|
||||
|
||||
let name = (data.name || '').toString().trim();
|
||||
let keys = ['name'];
|
||||
let values = [name];
|
||||
|
||||
if (id < 1) {
|
||||
return callback(new Error('Missing List ID'));
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
return callback(new Error('List Name must be set'));
|
||||
}
|
||||
|
||||
Object.keys(updates).forEach(key => {
|
||||
let value = updates[key].trim();
|
||||
key = tools.toDbKey(key);
|
||||
if (allowedKeys.indexOf(key) >= 0) {
|
||||
keys.push(key);
|
||||
values.push(value);
|
||||
}
|
||||
});
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
values.push(id);
|
||||
|
||||
connection.query('UPDATE lists SET ' + keys.map(key => key + '=?').join(', ') + ' WHERE id=? LIMIT 1', values, (err, result) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, result && result.affectedRows || false);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.delete = (id, callback) => {
|
||||
id = Number(id) || 0;
|
||||
|
||||
if (id < 1) {
|
||||
return callback(new Error('Missing List ID'));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('DELETE FROM lists WHERE id=? LIMIT 1', id, (err, result) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let affected = result && result.affectedRows || 0;
|
||||
|
||||
removeSubscriptionTable(id, err => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, affected);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function resolveCid(cid, callback) {
|
||||
cid = (cid || '').toString().trim();
|
||||
if (!cid) {
|
||||
return callback(new Error('Missing List CID'));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
connection.query('SELECT id FROM lists WHERE cid=?', [cid], (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, rows && rows[0] && rows[0].id || false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createSubscriptionTable(id, callback) {
|
||||
let query = 'CREATE TABLE `subscription__' + id + '` LIKE subscription';
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
connection.query(query, err => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function removeSubscriptionTable(id, callback) {
|
||||
let query = 'DROP TABLE IF EXISTS `subscription__' + id + '`';
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
connection.query(query, err => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, true);
|
||||
});
|
||||
});
|
||||
}
|
630
lib/models/segments.js
Normal file
630
lib/models/segments.js
Normal file
|
@ -0,0 +1,630 @@
|
|||
'use strict';
|
||||
|
||||
let tools = require('../tools');
|
||||
let db = require('../db');
|
||||
let fields = require('./fields');
|
||||
|
||||
module.exports.defaultColumns = [{
|
||||
column: 'email',
|
||||
name: 'Email address',
|
||||
type: 'string'
|
||||
}, {
|
||||
column: 'opt_in_country',
|
||||
name: 'Signup country',
|
||||
type: 'string'
|
||||
}, {
|
||||
column: 'created',
|
||||
name: 'Sign up date',
|
||||
type: 'date'
|
||||
}, {
|
||||
column: 'latest_open',
|
||||
name: 'Latest open',
|
||||
type: 'date'
|
||||
}, {
|
||||
column: 'latest_click',
|
||||
name: 'Latest click',
|
||||
type: 'date'
|
||||
}, {
|
||||
column: 'first_name',
|
||||
name: 'First name',
|
||||
type: 'string'
|
||||
}, {
|
||||
column: 'last_name',
|
||||
name: 'Last name',
|
||||
type: 'string'
|
||||
}];
|
||||
|
||||
module.exports.list = (listId, callback) => {
|
||||
listId = Number(listId) || 0;
|
||||
|
||||
if (listId < 1) {
|
||||
return callback(new Error('Missing List ID'));
|
||||
}
|
||||
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let query = 'SELECT * FROM segments WHERE list=? ORDER BY name';
|
||||
connection.query(query, [listId], (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let segments = (rows || []).map(tools.convertKeys);
|
||||
return callback(null, segments);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.get = (id, callback) => {
|
||||
id = Number(id) || 0;
|
||||
|
||||
if (id < 1) {
|
||||
return callback(new Error('Missing Segment ID'));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let query = 'SELECT * FROM segments WHERE id=? LIMIT 1';
|
||||
connection.query(query, [id], (err, rows) => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
if (!rows || !rows.length) {
|
||||
connection.release();
|
||||
return callback(new Error('Segment not found'));
|
||||
}
|
||||
|
||||
let segment = tools.convertKeys(rows[0]);
|
||||
|
||||
let query = 'SELECT * FROM segment_rules WHERE segment=? ORDER BY id ASC';
|
||||
connection.query(query, [id], (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
fields.list(segment.list, (err, fieldList) => {
|
||||
if (err || !fieldList) {
|
||||
fieldList = [];
|
||||
}
|
||||
|
||||
segment.columns = [].concat(module.exports.defaultColumns);
|
||||
fieldList.forEach(field => {
|
||||
if (field.column) {
|
||||
segment.columns.push({
|
||||
column: field.column,
|
||||
name: field.name,
|
||||
type: fields.genericTypes[field.type] || 'string'
|
||||
});
|
||||
}
|
||||
if (field.options) {
|
||||
field.options.forEach(subField => {
|
||||
if (subField.column) {
|
||||
segment.columns.push({
|
||||
column: subField.column,
|
||||
name: field.name + ': ' + subField.name,
|
||||
type: fields.genericTypes[subField.type] || 'string'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
segment.rules = (rows || []).map(rule => {
|
||||
rule = tools.convertKeys(rule);
|
||||
if (rule.value) {
|
||||
try {
|
||||
rule.value = JSON.parse(rule.value);
|
||||
} catch (E) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
if (!rule.value) {
|
||||
rule.value = {};
|
||||
}
|
||||
rule.columnType = segment.columns.filter(column => rule.column === column.column).pop() || {};
|
||||
rule.name = rule.columnType.name || '';
|
||||
switch (rule.columnType.type) {
|
||||
case 'number':
|
||||
case 'date':
|
||||
case 'birthday':
|
||||
if (rule.value.range) {
|
||||
rule.formatted = (rule.value.start || '') + ' … ' + (rule.value.end || '');
|
||||
} else {
|
||||
rule.formatted = rule.value.value || '';
|
||||
}
|
||||
break;
|
||||
case 'boolean':
|
||||
rule.formatted = rule.value.value ? 'Selected' : 'Not selected';
|
||||
break;
|
||||
default:
|
||||
rule.formatted = rule.value.value || '';
|
||||
}
|
||||
|
||||
return rule;
|
||||
});
|
||||
|
||||
return callback(null, segment);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.create = (listId, segment, callback) => {
|
||||
listId = Number(listId) || 0;
|
||||
|
||||
if (listId < 1) {
|
||||
return callback(new Error('Missing List ID'));
|
||||
}
|
||||
|
||||
segment = tools.convertKeys(segment);
|
||||
|
||||
segment.name = (segment.name || '').toString().trim();
|
||||
segment.type = Number(segment.type) || 0;
|
||||
|
||||
if (!segment.name) {
|
||||
return callback(new Error('Field Name must be set'));
|
||||
}
|
||||
|
||||
if (segment.type <= 0) {
|
||||
return callback(new Error('Invalid segment rule type'));
|
||||
}
|
||||
|
||||
let keys = ['list', 'name', 'type'];
|
||||
let values = [listId, segment.name, segment.type];
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let query = 'INSERT INTO segments (`' + keys.join('`, `') + '`) VALUES (' + values.map(() => '?').join(',') + ')';
|
||||
connection.query(query, values, (err, result) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, result && result.insertId || false);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.update = (id, updates, callback) => {
|
||||
updates = updates || {};
|
||||
id = Number(id) || 0;
|
||||
|
||||
if (id < 1) {
|
||||
return callback(new Error('Missing Segment ID'));
|
||||
}
|
||||
|
||||
let segment = tools.convertKeys(updates);
|
||||
|
||||
segment.name = (segment.name || '').toString().trim();
|
||||
segment.type = Number(segment.type) || 0;
|
||||
|
||||
if (!segment.name) {
|
||||
return callback(new Error('Field Name must be set'));
|
||||
}
|
||||
|
||||
if (segment.type <= 0) {
|
||||
return callback(new Error('Invalid segment rule type'));
|
||||
}
|
||||
|
||||
let keys = ['name', 'type'];
|
||||
let values = [segment.name, segment.type];
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
values.push(id);
|
||||
|
||||
connection.query('UPDATE segments SET ' + keys.map(key => '`' + key + '`=?').join(', ') + ' WHERE id=? LIMIT 1', values, (err, result) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, result && result.affectedRows || false);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.delete = (id, callback) => {
|
||||
id = Number(id) || 0;
|
||||
|
||||
if (id < 1) {
|
||||
return callback(new Error('Missing Segment ID'));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('DELETE FROM segments WHERE id=? LIMIT 1', [id], err => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, true);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.createRule = (segmentId, rule, callback) => {
|
||||
segmentId = Number(segmentId) || 0;
|
||||
|
||||
if (segmentId < 1) {
|
||||
return callback(new Error('Missing Segment ID'));
|
||||
}
|
||||
|
||||
rule = tools.convertKeys(rule);
|
||||
|
||||
module.exports.get(segmentId, (err, segment) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!segment) {
|
||||
return callback(new Error('Selected segment not found'));
|
||||
}
|
||||
|
||||
let column = segment.columns.filter(column => column.column === rule.column).pop();
|
||||
if (!column) {
|
||||
return callback(new Error('Invalid rule type'));
|
||||
}
|
||||
|
||||
let value;
|
||||
|
||||
switch (column.type) {
|
||||
case 'date':
|
||||
case 'birthday':
|
||||
case 'number':
|
||||
if (rule.range) {
|
||||
value = {
|
||||
range: true,
|
||||
start: rule.start,
|
||||
end: rule.end
|
||||
};
|
||||
} else {
|
||||
value = {
|
||||
value: rule.value
|
||||
};
|
||||
}
|
||||
break;
|
||||
case 'boolean':
|
||||
value = {
|
||||
value: rule.value ? 1 : 0
|
||||
};
|
||||
break;
|
||||
default:
|
||||
value = {
|
||||
value: rule.value
|
||||
};
|
||||
}
|
||||
|
||||
let keys = ['segment', 'column', 'value'];
|
||||
let values = [segment.id, rule.column, JSON.stringify(value)];
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let query = 'INSERT INTO segment_rules (`' + keys.join('`, `') + '`) VALUES (' + values.map(() => '?').join(',') + ')';
|
||||
connection.query(query, values, (err, result) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, result && result.insertId || false);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.getRule = (id, callback) => {
|
||||
id = Number(id) || 0;
|
||||
|
||||
if (id < 1) {
|
||||
return callback(new Error('Missing Rule ID'));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let query = 'SELECT * FROM segment_rules WHERE id=? LIMIT 1';
|
||||
connection.query(query, [id], (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!rows || !rows.length) {
|
||||
return callback(new Error('Specified rule not found'));
|
||||
}
|
||||
|
||||
let rule = tools.convertKeys(rows[0]);
|
||||
|
||||
module.exports.get(rule.segment, (err, segment) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!segment) {
|
||||
return callback(new Error('Specified segment not found'));
|
||||
}
|
||||
|
||||
if (rule.value) {
|
||||
try {
|
||||
rule.value = JSON.parse(rule.value);
|
||||
} catch (E) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
if (!rule.value) {
|
||||
rule.value = {};
|
||||
}
|
||||
|
||||
rule.columnType = segment.columns.filter(column => rule.column === column.column).pop() || {};
|
||||
|
||||
rule.name = rule.columnType.name || '';
|
||||
switch (rule.columnType.type) {
|
||||
case 'number':
|
||||
case 'date':
|
||||
case 'birthday':
|
||||
if (rule.value.range) {
|
||||
rule.formatted = (rule.value.start || '') + ' … ' + (rule.value.end || '');
|
||||
} else {
|
||||
rule.formatted = rule.value.value || '';
|
||||
}
|
||||
break;
|
||||
case 'boolean':
|
||||
rule.formatted = rule.value.value ? 'Selected' : 'Not selected';
|
||||
break;
|
||||
default:
|
||||
rule.formatted = rule.value.value || '';
|
||||
}
|
||||
|
||||
return callback(null, rule);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.updateRule = (id, rule, callback) => {
|
||||
id = Number(id) || 0;
|
||||
|
||||
if (id < 1) {
|
||||
return callback(new Error('Missing Rule ID'));
|
||||
}
|
||||
|
||||
rule = tools.convertKeys(rule);
|
||||
|
||||
module.exports.getRule(id, (err, existingRule) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!existingRule) {
|
||||
return callback(new Error('Selected rule not found'));
|
||||
}
|
||||
|
||||
module.exports.get(existingRule.segment, (err, segment) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!segment) {
|
||||
return callback(new Error('Selected segment not found'));
|
||||
}
|
||||
|
||||
let column = segment.columns.filter(column => column.column === existingRule.column).pop();
|
||||
if (!column) {
|
||||
return callback(new Error('Invalid rule type'));
|
||||
}
|
||||
|
||||
let value;
|
||||
switch (column.type) {
|
||||
case 'date':
|
||||
case 'birthday':
|
||||
case 'number':
|
||||
if (rule.range) {
|
||||
value = {
|
||||
range: true,
|
||||
start: rule.start,
|
||||
end: rule.end
|
||||
};
|
||||
} else {
|
||||
value = {
|
||||
value: rule.value
|
||||
};
|
||||
}
|
||||
break;
|
||||
case 'boolean':
|
||||
value = {
|
||||
value: rule.value ? 1 : 0
|
||||
};
|
||||
break;
|
||||
default:
|
||||
value = {
|
||||
value: rule.value
|
||||
};
|
||||
}
|
||||
|
||||
let keys = ['value'];
|
||||
let values = [JSON.stringify(value)];
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
values.push(id);
|
||||
|
||||
connection.query('UPDATE segment_rules SET ' + keys.map(key => '`' + key + '`=?').join(', ') + ' WHERE id=? LIMIT 1', values, (err, result) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, result && result.affectedRows || false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.deleteRule = (id, callback) => {
|
||||
id = Number(id) || 0;
|
||||
|
||||
if (id < 1) {
|
||||
return callback(new Error('Missing Rule ID'));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('DELETE FROM segment_rules WHERE id=? LIMIT 1', [id], err => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, true);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.getQuery = (id, callback) => {
|
||||
module.exports.get(id, (err, segment) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!segment) {
|
||||
return callback(new Error('Segment not found'));
|
||||
}
|
||||
|
||||
let query = [];
|
||||
let values = [];
|
||||
|
||||
let getDate = (value, nextDay) => {
|
||||
let parts = value.trim().split(/\D/);
|
||||
let year = Number(parts.shift()) || 0;
|
||||
let month = Number(parts.shift()) || 0;
|
||||
let day = Number(parts.shift()) || 0;
|
||||
if (!year || !month || !day) {
|
||||
return false;
|
||||
}
|
||||
return new Date(Date.UTC(year, month - 1, day + (nextDay ? 1 : 0)));
|
||||
};
|
||||
|
||||
segment.rules.forEach(rule => {
|
||||
switch (rule.columnType.type) {
|
||||
case 'string':
|
||||
query.push('`' + rule.columnType.column + '` LIKE ?');
|
||||
values.push(rule.value.value);
|
||||
break;
|
||||
case 'boolean':
|
||||
query.push('`' + rule.columnType.column + '` = ?');
|
||||
values.push(rule.value.value);
|
||||
break;
|
||||
case 'number':
|
||||
if (rule.value.range) {
|
||||
query.push('`' + rule.columnType.column + '` >= ?');
|
||||
query.push('`' + rule.columnType.column + '` < ?');
|
||||
values.push(rule.value.start);
|
||||
values.push(rule.value.end);
|
||||
} else {
|
||||
query.push('`' + rule.columnType.column + '` = ?');
|
||||
values.push(rule.value.value);
|
||||
}
|
||||
break;
|
||||
case 'birthday':
|
||||
if (rule.value.range) {
|
||||
query.push('`' + rule.columnType.column + '` >= ?');
|
||||
query.push('`' + rule.columnType.column + '` < ?');
|
||||
values.push(getDate('2000-' + rule.value.start));
|
||||
values.push(getDate('2000-' + rule.value.end, true));
|
||||
} else {
|
||||
query.push('`' + rule.columnType.column + '` >= ?');
|
||||
query.push('`' + rule.columnType.column + '` < ?');
|
||||
values.push(getDate('2000-' + rule.value.value));
|
||||
values.push(getDate('2000-' + rule.value.value, true));
|
||||
}
|
||||
break;
|
||||
case 'date':
|
||||
if (rule.value.range) {
|
||||
query.push('`' + rule.columnType.column + '` >= ?');
|
||||
query.push('`' + rule.columnType.column + '` < ?');
|
||||
values.push(getDate(rule.value.start));
|
||||
values.push(getDate(rule.value.end, true));
|
||||
} else {
|
||||
query.push('`' + rule.columnType.column + '` >= ?');
|
||||
query.push('`' + rule.columnType.column + '` < ?');
|
||||
values.push(getDate(rule.value.value));
|
||||
values.push(getDate(rule.value.value, true));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return callback(null, {
|
||||
where: query.join(segment.type === 1 ? ' AND ' : ' OR ') || '1',
|
||||
values
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.subscribers = (id, onlySubscribed, callback) => {
|
||||
module.exports.get(id, (err, segment) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (!segment) {
|
||||
return callback(new Error('Segment not found'));
|
||||
}
|
||||
module.exports.getQuery(id, (err, queryData) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let query;
|
||||
if (!onlySubscribed) {
|
||||
query = 'SELECT COUNT(id) AS `count` FROM `subscription__' + segment.list + '` WHERE ' + queryData.where + ' LIMIT 1';
|
||||
} else {
|
||||
query = 'SELECT COUNT(id) AS `count` FROM `subscription__' + segment.list + '` WHERE `status`=1 ' + (queryData.where ? ' AND (' + queryData.where + ')' : '') + ' LIMIT 1';
|
||||
}
|
||||
|
||||
connection.query(query, queryData.values, (err, rows) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
let count = rows && rows[0] && rows[0].count || 0;
|
||||
return callback(null, count);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
75
lib/models/settings.js
Normal file
75
lib/models/settings.js
Normal file
|
@ -0,0 +1,75 @@
|
|||
'use strict';
|
||||
|
||||
let tools = require('../tools');
|
||||
let db = require('../db');
|
||||
|
||||
module.exports = {
|
||||
list: listValues,
|
||||
get: getValue,
|
||||
set: setValue
|
||||
};
|
||||
|
||||
function listValues(filter, callback) {
|
||||
if (!callback && typeof filter === 'function') {
|
||||
callback = filter;
|
||||
filter = false;
|
||||
}
|
||||
|
||||
filter = [].concat(filter || []).map(key => tools.toDbKey(key));
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let query;
|
||||
|
||||
if (filter.length) {
|
||||
query = 'SELECT * FROM settings WHERE `key` IN (' + filter.map(() => '?').join(',') + ')';
|
||||
} else {
|
||||
query = 'SELECT * FROM settings';
|
||||
}
|
||||
|
||||
connection.query(query, filter, (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
let settings = {};
|
||||
(rows || []).forEach(row => {
|
||||
settings[row.key] = row.value;
|
||||
});
|
||||
return callback(null, tools.convertKeys(settings));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getValue(key, callback) {
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
connection.query('SELECT `value` FROM settings WHERE `key`=?', [tools.toDbKey(key)], (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, rows && rows[0] && rows[0].value || false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setValue(key, value, callback) {
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
connection.query('INSERT INTO settings (`key`, `value`) VALUES (?,?) ON DUPLICATE KEY UPDATE `key`=?, `value`=?', [key, value, key, value], (err, response) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, response && response.insertId || 0);
|
||||
});
|
||||
});
|
||||
}
|
794
lib/models/subscriptions.js
Normal file
794
lib/models/subscriptions.js
Normal file
|
@ -0,0 +1,794 @@
|
|||
'use strict';
|
||||
|
||||
let db = require('../db');
|
||||
let shortid = require('shortid');
|
||||
let tools = require('../tools');
|
||||
let fields = require('./fields');
|
||||
let geoip = require('geoip-ultralight');
|
||||
let segments = require('./segments');
|
||||
|
||||
module.exports.list = (listId, start, limit, callback) => {
|
||||
listId = Number(listId) || 0;
|
||||
if (!listId) {
|
||||
return callback(new Error('Missing List ID'));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('SELECT SQL_CALC_FOUND_ROWS * FROM `subscription__' + listId + '` ORDER BY email LIMIT ? OFFSET ?', [limit, start], (err, rows) => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
connection.query('SELECT FOUND_ROWS() AS total', (err, total) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let subscriptions = rows.map(row => tools.convertKeys(row));
|
||||
return callback(null, subscriptions, total && total[0] && total[0].total);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.filter = (listId, request, columns, segmentId, callback) => {
|
||||
listId = Number(listId) || 0;
|
||||
segmentId = Number(segmentId) || 0;
|
||||
|
||||
if (!listId) {
|
||||
return callback(new Error('Missing List ID'));
|
||||
}
|
||||
|
||||
let processQuery = queryData => {
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let query = 'SELECT COUNT(id) AS total FROM `subscription__' + listId + '`';
|
||||
let values = [];
|
||||
|
||||
if (queryData.where) {
|
||||
query += ' WHERE ' + queryData.where;
|
||||
values = values.concat(queryData.values || []);
|
||||
}
|
||||
|
||||
connection.query(query, values, (err, total) => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
total = total && total[0] && total[0].total || 0;
|
||||
|
||||
let ordering = [];
|
||||
|
||||
if (request.order && request.order.length) {
|
||||
|
||||
request.order.forEach(order => {
|
||||
let orderField = columns[Number(order.column)];
|
||||
let orderDirection = (order.dir || '').toString().toLowerCase() === 'desc' ? 'DESC' : 'ASC';
|
||||
if (orderField) {
|
||||
ordering.push('`' + orderField + '` ' + orderDirection);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!ordering.length) {
|
||||
ordering.push('`email` ASC');
|
||||
}
|
||||
|
||||
let args = [Number(request.length) || 50, Number(request.start) || 0];
|
||||
let query;
|
||||
|
||||
if (request.search && request.search.value) {
|
||||
query = 'SELECT SQL_CALC_FOUND_ROWS * FROM `subscription__' + listId + '` WHERE email LIKE ? OR first_name LIKE ? OR last_name LIKE ? ' + (queryData.where ? ' AND (' + queryData.where + ')' : '') + ' ORDER BY ' + ordering.join(', ') + ' LIMIT ? OFFSET ?';
|
||||
|
||||
let searchVal = '%' + request.search.value.replace(/\\/g, '\\\\').replace(/([%_])/g, '\\$1') + '%';
|
||||
args = [searchVal, searchVal, searchVal].concat(queryData.values || []).concat(args);
|
||||
} else {
|
||||
query = 'SELECT SQL_CALC_FOUND_ROWS * FROM `subscription__' + listId + '` WHERE 1 ' + (queryData.where ? ' AND (' + queryData.where + ')' : '') + ' ORDER BY ' + ordering.join(', ') + ' LIMIT ? OFFSET ?';
|
||||
args = [].concat(queryData.values || []).concat(args);
|
||||
}
|
||||
|
||||
connection.query(query, args, (err, rows) => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
connection.query('SELECT FOUND_ROWS() AS total', (err, filteredTotal) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let subscriptions = rows.map(row => tools.convertKeys(row));
|
||||
|
||||
filteredTotal = filteredTotal && filteredTotal[0] && filteredTotal[0].total || 0;
|
||||
return callback(null, subscriptions, total, filteredTotal);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if (segmentId) {
|
||||
segments.getQuery(segmentId, (err, queryData) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
processQuery(queryData);
|
||||
});
|
||||
} else {
|
||||
processQuery(false);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
module.exports.addConfirmation = (listId, email, data, callback) => {
|
||||
let cid = shortid.generate();
|
||||
|
||||
tools.validateEmail(email, false, err => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let query = 'INSERT INTO confirmations (cid, list, email, data) VALUES (?,?,?,?)';
|
||||
connection.query(query, [cid, listId, email, JSON.stringify(data || {})], (err, result) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, result && cid || false);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.subscribe = (cid, optInIp, callback) => {
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let query = 'SELECT * FROM confirmations WHERE cid=? LIMIT 1';
|
||||
connection.query(query, [cid], (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!rows || !rows.length) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
let subscription;
|
||||
let listId = rows[0].list;
|
||||
let email = rows[0].email;
|
||||
try {
|
||||
subscription = JSON.parse(rows[0].data);
|
||||
} catch (E) {
|
||||
subscription = {};
|
||||
}
|
||||
|
||||
let optInCountry = geoip.lookupCountry(optInIp) || null;
|
||||
module.exports.insert(listId, {
|
||||
email,
|
||||
cid,
|
||||
optInIp,
|
||||
optInCountry,
|
||||
status: 1
|
||||
}, subscription, err => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
connection.query('DELETE FROM confirmations WHERE `cid`=? LIMIT 1', [cid], () => {
|
||||
connection.release();
|
||||
callback(null, {
|
||||
list: listId,
|
||||
email
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.insert = (listId, meta, subscription, callback) => {
|
||||
|
||||
meta = tools.convertKeys(meta);
|
||||
subscription = tools.convertKeys(subscription);
|
||||
|
||||
meta.email = meta.email || subscription.email;
|
||||
meta.cid = meta.cid || shortid.generate();
|
||||
|
||||
fields.list(listId, (err, fieldList) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let insertKeys = ['email', 'cid', 'opt_in_ip', 'opt_in_country', 'imported'];
|
||||
let insertValues = [meta.email, meta.cid, meta.optInIp || null, meta.optInCountry || null, meta.imported || null];
|
||||
let keys = [];
|
||||
let values = [];
|
||||
|
||||
let allowedKeys = ['first_name', 'last_name'];
|
||||
Object.keys(subscription).forEach(key => {
|
||||
let value = subscription[key];
|
||||
key = tools.toDbKey(key);
|
||||
if (allowedKeys.indexOf(key) >= 0) {
|
||||
keys.push(key);
|
||||
values.push(value);
|
||||
}
|
||||
});
|
||||
|
||||
fields.getValues(fields.getRow(fieldList, subscription, true, true), true).forEach(field => {
|
||||
keys.push(field.key);
|
||||
values.push(field.value);
|
||||
});
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.beginTransaction(err => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let query = 'SELECT id, status FROM `subscription__' + listId + '` WHERE email=? OR cid=? LIMIT 1';
|
||||
connection.query(query, [meta.email, meta.cid], (err, rows) => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
|
||||
let query;
|
||||
let queryArgs;
|
||||
let existing = rows && rows[0] || false;
|
||||
let entryId = existing ? existing.id : false;
|
||||
|
||||
meta.status = meta.status || (existing ? existing.status : 1);
|
||||
|
||||
let statusChange = !existing || existing.status !== meta.status;
|
||||
let statusDirection;
|
||||
|
||||
if (statusChange) {
|
||||
keys.push('status', 'status_change');
|
||||
values.push(meta.status, new Date());
|
||||
statusDirection = !existing ? (meta.status === 1 ? '+' : '-') : (existing.status === 1 ? '-' : '+');
|
||||
}
|
||||
|
||||
if (!existing) {
|
||||
// insert as new
|
||||
keys = insertKeys.concat(keys);
|
||||
queryArgs = values = insertValues.concat(values);
|
||||
query = 'INSERT INTO `subscription__' + listId + '` (`' + keys.join('`, `') + '`) VALUES (' + keys.map(() => '?').join(',') + ')';
|
||||
} else {
|
||||
// update existing
|
||||
queryArgs = values.concat(existing.id);
|
||||
query = 'UPDATE `subscription__' + listId + '` SET ' + keys.map(key => '`' + key + '`=?') + ' WHERE id=? LIMIT 1';
|
||||
}
|
||||
|
||||
connection.query(query, queryArgs, (err, result) => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
|
||||
entryId = result.insertId || entryId;
|
||||
|
||||
if (statusChange) {
|
||||
connection.query('UPDATE lists SET `subscribers`=`subscribers`' + statusDirection + '1 WHERE id=?', [listId], err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
connection.commit(err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
connection.release();
|
||||
return callback(null, entryId);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
connection.commit(err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
connection.release();
|
||||
return callback(null, entryId);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.get = (listId, cid, callback) => {
|
||||
cid = (cid || '').toString().trim();
|
||||
|
||||
if (!cid) {
|
||||
return callback(new Error('Missing Subbscription ID'));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('SELECT * FROM `subscription__' + listId + '` WHERE cid=?', [cid], (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!rows || !rows.length) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
let list = tools.convertKeys(rows[0]);
|
||||
return callback(null, list);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.update = (listId, cid, updates, allowEmail, callback) => {
|
||||
updates = tools.convertKeys(updates);
|
||||
listId = Number(listId) || 0;
|
||||
cid = (cid || '').toString().trim();
|
||||
|
||||
let keys = [];
|
||||
let values = [];
|
||||
|
||||
if (listId < 1) {
|
||||
return callback(new Error('Missing List ID'));
|
||||
}
|
||||
|
||||
if (!cid) {
|
||||
return callback(new Error('Missing subscription ID'));
|
||||
}
|
||||
|
||||
fields.list(listId, (err, fieldList) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let allowedKeys = ['first_name', 'last_name'];
|
||||
|
||||
if (allowEmail) {
|
||||
allowedKeys.unshift('email');
|
||||
}
|
||||
|
||||
Object.keys(updates).forEach(key => {
|
||||
let value = updates[key];
|
||||
key = tools.toDbKey(key);
|
||||
if (allowedKeys.indexOf(key) >= 0) {
|
||||
keys.push(key);
|
||||
values.push(value);
|
||||
}
|
||||
});
|
||||
|
||||
fields.getValues(fields.getRow(fieldList, updates, true, true), true).forEach(field => {
|
||||
keys.push(field.key);
|
||||
values.push(field.value);
|
||||
});
|
||||
|
||||
if (!values.length) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
values.push(cid);
|
||||
connection.query('UPDATE `subscription__' + listId + '` SET ' + keys.map(key => '`' + key + '`=?').join(', ') + ' WHERE `cid`=? LIMIT 1', values, (err, result) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, result && result.affectedRows || false);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.unsubscribe = (listId, email, campaignId, callback) => {
|
||||
listId = Number(listId) || 0;
|
||||
email = (email || '').toString().trim();
|
||||
|
||||
campaignId = (campaignId || '').toString().trim() || false;
|
||||
|
||||
if (listId < 1) {
|
||||
return callback(new Error('Missing List ID'));
|
||||
}
|
||||
|
||||
if (!email) {
|
||||
return callback(new Error('Missing email address'));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('SELECT id, status FROM `subscription__' + listId + '` WHERE `email`=?', [email], (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (!rows || !rows.length || rows[0].status !== 1) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
let id = rows[0].id;
|
||||
module.exports.changeStatus(id, listId, campaignId, 2, callback);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.changeStatus = (id, listId, campaignId, status, callback) => {
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
connection.beginTransaction(err => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('SELECT `status` FROM `subscription__' + listId + '` WHERE id=? LIMIT 1', [id], (err, rows) => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
|
||||
if (!rows || !rows.length) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(null, false);
|
||||
});
|
||||
}
|
||||
|
||||
let oldStatus = rows[0].status;
|
||||
let statusChange = oldStatus !== status;
|
||||
let statusDirection;
|
||||
|
||||
if (!statusChange) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(null, true);
|
||||
});
|
||||
}
|
||||
|
||||
if (statusChange && oldStatus === 1 || status === 1) {
|
||||
statusDirection = status === 1 ? '+' : '-';
|
||||
}
|
||||
|
||||
connection.query('UPDATE `subscription__' + listId + '` SET `status`=?, `status_change`=NOW() WHERE id=? LIMIT 1', [status, id], err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
|
||||
if (!statusDirection) {
|
||||
return connection.commit(err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
connection.release();
|
||||
return callback(null, true);
|
||||
});
|
||||
}
|
||||
|
||||
connection.query('UPDATE `lists` SET `subscribers`=`subscribers`' + statusDirection + '1 WHERE id=? LIMIT 1', [listId], err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
|
||||
if (!campaignId) {
|
||||
return connection.commit(err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
connection.release();
|
||||
return callback(null, true);
|
||||
});
|
||||
}
|
||||
|
||||
connection.query('UPDATE `campaigns` SET `unsubscribed`=`unsubscribed`+1 WHERE `cid`=? LIMIT 1', [campaignId], err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
return connection.commit(err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
connection.release();
|
||||
return callback(null, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.delete = (listId, cid, callback) => {
|
||||
listId = Number(listId) || 0;
|
||||
cid = (cid || '').toString().trim();
|
||||
|
||||
if (listId < 1) {
|
||||
return callback(new Error('Missing List ID'));
|
||||
}
|
||||
|
||||
if (!cid) {
|
||||
return callback(new Error('Missing subscription ID'));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('SELECT id, email, status FROM `subscription__' + listId + '` WHERE cid=? LIMIT 1', [cid], (err, rows) => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let subscription = rows && rows[0];
|
||||
if (!subscription) {
|
||||
connection.release();
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
connection.beginTransaction(err => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('DELETE FROM `subscription__' + listId + '` WHERE cid=? LIMIT 1', [cid], err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
|
||||
if (subscription.status !== 1) {
|
||||
return connection.commit(err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
connection.release();
|
||||
return callback(null, subscription.email);
|
||||
});
|
||||
}
|
||||
|
||||
connection.query('UPDATE lists SET subscribers=subscribers-1 WHERE id=? LIMIT 1', [listId], err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
connection.commit(err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
connection.release();
|
||||
return callback(null, subscription.email);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.createImport = (listId, type, path, size, delimiter, mapping, callback) => {
|
||||
listId = Number(listId) || 0;
|
||||
type = Number(type) || 1;
|
||||
|
||||
if (listId < 1) {
|
||||
return callback(new Error('Missing List ID'));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
let query = 'INSERT INTO importer (`list`, `type`, `path`, `size`, `delimiter`, `mapping`) VALUES(?,?,?,?,?,?)';
|
||||
connection.query(query, [listId, type, path, size, delimiter, JSON.stringify(mapping)], (err, result) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, result && result.insertId || false);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.updateImport = (listId, importId, data, callback) => {
|
||||
listId = Number(listId) || 0;
|
||||
importId = Number(importId) || 0;
|
||||
|
||||
if (listId < 1) {
|
||||
return callback(new Error('Missing List ID'));
|
||||
}
|
||||
|
||||
if (importId < 1) {
|
||||
return callback(new Error('Missing Import ID'));
|
||||
}
|
||||
|
||||
let keys = [];
|
||||
let values = [];
|
||||
|
||||
let allowedKeys = ['type', 'path', 'size', 'delimiter', 'status', 'error', 'processed', 'mapping', 'finished'];
|
||||
Object.keys(data).forEach(key => {
|
||||
let value = data[key];
|
||||
key = tools.toDbKey(key);
|
||||
if (allowedKeys.indexOf(key) >= 0) {
|
||||
keys.push(key);
|
||||
values.push(value);
|
||||
}
|
||||
});
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
let query = 'UPDATE importer SET ' + keys.map(key => '`' + key + '`=?') + ' WHERE id=? AND list=? LIMIT 1';
|
||||
connection.query(query, values.concat([importId, listId]), (err, result) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, result && result.affectedRows || false);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.getImport = (listId, importId, callback) => {
|
||||
listId = Number(listId) || 0;
|
||||
importId = Number(importId) || 0;
|
||||
|
||||
if (listId < 1) {
|
||||
return callback(new Error('Missing List ID'));
|
||||
}
|
||||
|
||||
if (importId < 1) {
|
||||
return callback(new Error('Missing Import ID'));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
let query = 'SELECT * FROM importer WHERE id=? AND list=? LIMIT 1';
|
||||
connection.query(query, [importId, listId], (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!rows || !rows.length) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
let importer = tools.convertKeys(rows[0]);
|
||||
try {
|
||||
importer.mapping = JSON.parse(importer.mapping);
|
||||
} catch (E) {
|
||||
importer.mapping = {
|
||||
columns: []
|
||||
};
|
||||
}
|
||||
|
||||
return callback(null, importer);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.listImports = (listId, callback) => {
|
||||
listId = Number(listId) || 0;
|
||||
|
||||
if (listId < 1) {
|
||||
return callback(new Error('Missing List ID'));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
let query = 'SELECT * FROM importer WHERE list=? AND status > 0 ORDER BY id DESC';
|
||||
connection.query(query, [listId], (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!rows || !rows.length) {
|
||||
return callback(null, []);
|
||||
}
|
||||
|
||||
let imports = rows.map(row => {
|
||||
let importer = tools.convertKeys(row);
|
||||
try {
|
||||
importer.mapping = JSON.parse(importer.mapping);
|
||||
} catch (E) {
|
||||
importer.mapping = {
|
||||
columns: []
|
||||
};
|
||||
}
|
||||
return importer;
|
||||
});
|
||||
|
||||
return callback(null, imports);
|
||||
});
|
||||
});
|
||||
};
|
182
lib/models/templates.js
Normal file
182
lib/models/templates.js
Normal file
|
@ -0,0 +1,182 @@
|
|||
'use strict';
|
||||
|
||||
let db = require('../db');
|
||||
let tools = require('../tools');
|
||||
|
||||
let allowedKeys = ['description', 'html', 'text'];
|
||||
|
||||
module.exports.list = (start, limit, callback) => {
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('SELECT SQL_CALC_FOUND_ROWS * FROM templates ORDER BY name LIMIT ? OFFSET ?', [limit, start], (err, rows) => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
connection.query('SELECT FOUND_ROWS() AS total', (err, total) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, rows, total && total[0] && total[0].total);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.quicklist = callback => {
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('SELECT id, name FROM templates ORDER BY name LIMIT 1000', (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, (rows || []).map(tools.convertKeys));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.get = (id, callback) => {
|
||||
id = Number(id) || 0;
|
||||
|
||||
if (id < 1) {
|
||||
return callback(new Error('Missing Template ID'));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('SELECT * FROM templates WHERE id=?', [id], (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!rows || !rows.length) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
let template = tools.convertKeys(rows[0]);
|
||||
return callback(null, template);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.create = (template, callback) => {
|
||||
|
||||
let data = tools.convertKeys(template);
|
||||
|
||||
if (!(data.name || '').toString().trim()) {
|
||||
return callback(new Error('Template Name must be set'));
|
||||
}
|
||||
|
||||
let name = (template.name || '').toString().trim();
|
||||
|
||||
let keys = ['name'];
|
||||
let values = [name];
|
||||
|
||||
Object.keys(template).forEach(key => {
|
||||
let value = template[key].trim();
|
||||
key = tools.toDbKey(key);
|
||||
if (allowedKeys.indexOf(key) >= 0) {
|
||||
keys.push(key);
|
||||
values.push(value);
|
||||
}
|
||||
});
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let query = 'INSERT INTO templates (' + keys.join(', ') + ') VALUES (' + values.map(() => '?').join(',') + ')';
|
||||
connection.query(query, values, (err, result) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let templateId = result && result.insertId || false;
|
||||
return callback(null, templateId);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.update = (id, updates, callback) => {
|
||||
updates = updates || {};
|
||||
id = Number(id) || 0;
|
||||
|
||||
let data = tools.convertKeys(updates);
|
||||
|
||||
if (id < 1) {
|
||||
return callback(new Error('Missing Template ID'));
|
||||
}
|
||||
|
||||
if (!(data.name || '').toString().trim()) {
|
||||
return callback(new Error('Template Name must be set'));
|
||||
}
|
||||
|
||||
let name = (updates.name || '').toString().trim();
|
||||
let keys = ['name'];
|
||||
let values = [name];
|
||||
|
||||
Object.keys(updates).forEach(key => {
|
||||
let value = updates[key].trim();
|
||||
key = tools.toDbKey(key);
|
||||
if (allowedKeys.indexOf(key) >= 0) {
|
||||
keys.push(key);
|
||||
values.push(value);
|
||||
}
|
||||
});
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
values.push(id);
|
||||
|
||||
connection.query('UPDATE templates SET ' + keys.map(key => key + '=?').join(', ') + ' WHERE id=? LIMIT 1', values, (err, result) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, result && result.affectedRows || false);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.delete = (id, callback) => {
|
||||
id = Number(id) || 0;
|
||||
|
||||
if (id < 1) {
|
||||
return callback(new Error('Missing Template ID'));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('DELETE FROM templates WHERE id=? LIMIT 1', id, (err, result) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let affected = result && result.affectedRows || 0;
|
||||
|
||||
return callback(null, affected);
|
||||
});
|
||||
});
|
||||
};
|
285
lib/models/users.js
Normal file
285
lib/models/users.js
Normal file
|
@ -0,0 +1,285 @@
|
|||
'use strict';
|
||||
|
||||
let log = require('npmlog');
|
||||
|
||||
let bcrypt = require('bcrypt-nodejs');
|
||||
let db = require('../db');
|
||||
let tools = require('../tools');
|
||||
let mailer = require('../mailer');
|
||||
let settings = require('./settings');
|
||||
let crypto = require('crypto');
|
||||
let urllib = require('url');
|
||||
|
||||
/**
|
||||
* Fetches user by ID value
|
||||
*
|
||||
* @param {Number} id User id
|
||||
* @param {Function} callback Return an error or an user object
|
||||
*/
|
||||
module.exports.get = (id, callback) => {
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
connection.query('SELECT id, username, email FROM users WHERE id=? LIMIT 1', [id], (err, rows) => {
|
||||
connection.release();
|
||||
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!rows.length) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
let user = tools.convertKeys(rows[0]);
|
||||
return callback(null, user);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches user by username and password
|
||||
*
|
||||
* @param {String} username
|
||||
* @param {String} password
|
||||
* @param {Function} callback Return an error or authenticated user
|
||||
*/
|
||||
module.exports.authenticate = (username, password, callback) => {
|
||||
|
||||
let login = (connection, callback) => {
|
||||
connection.query('SELECT id, password FROM users WHERE username=? OR email=? LIMIT 1', [username, username], (err, rows) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!rows.length) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
bcrypt.compare(password, rows[0].password, (err, result) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (!result) {
|
||||
return callback(null, false);
|
||||
}
|
||||
return callback(null, {
|
||||
id: rows[0].id,
|
||||
username
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
login(connection, (err, user) => {
|
||||
connection.release();
|
||||
callback(err, user);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates user password
|
||||
*
|
||||
* @param {Object} id User ID
|
||||
* @param {Object} updates
|
||||
* @param {Function} Return an error or success/fail
|
||||
*/
|
||||
module.exports.update = (id, updates, callback) => {
|
||||
|
||||
if (!updates.email) {
|
||||
return callback(new Error('Email Address must be set'));
|
||||
}
|
||||
|
||||
let update = (connection, callback) => {
|
||||
|
||||
connection.query('SELECT password FROM users WHERE id=? LIMIT 1', [id], (err, rows) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!rows.length) {
|
||||
return callback('Failed to check user data');
|
||||
}
|
||||
|
||||
let keys = ['email'];
|
||||
let values = [updates.email];
|
||||
|
||||
let finalize = () => {
|
||||
values.push(id);
|
||||
connection.query('UPDATE users SET ' + keys.map(key => key + '=?').join(', ') + ' WHERE id=? LIMIT 1', values, (err, result) => {
|
||||
if (err) {
|
||||
if (err.code === 'ER_DUP_ENTRY') {
|
||||
err = new Error('Can\'t change email as another user with the same email address already exists');
|
||||
}
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, result.affectedRows);
|
||||
});
|
||||
};
|
||||
|
||||
if (!updates.password && !updates.password2) {
|
||||
return finalize();
|
||||
}
|
||||
|
||||
bcrypt.compare(updates.currentPassword, rows[0].password, (err, result) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (!result) {
|
||||
return callback('Incorrect current password');
|
||||
}
|
||||
|
||||
if (!updates.password) {
|
||||
return callback(new Error('New password not set'));
|
||||
}
|
||||
|
||||
if (updates.password !== updates.password2) {
|
||||
return callback(new Error('Passwords do not match'));
|
||||
}
|
||||
|
||||
bcrypt.hash(updates.password, null, null, (err, hash) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
keys.push('password');
|
||||
values.push(hash);
|
||||
|
||||
finalize();
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
tools.validateEmail(updates.email, false, err => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
update(connection, (err, updated) => {
|
||||
connection.release();
|
||||
callback(err, updated);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.sendReset = (username, callback) => {
|
||||
username = (username || '').toString().trim();
|
||||
|
||||
if (!username) {
|
||||
return callback(new Error('Username must be set'));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
connection.query('SELECT id, email, username FROM users WHERE username=? OR email=? LIMIT 1', [username, username], (err, rows) => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!rows.length) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
let resetToken = crypto.randomBytes(16).toString('base64').replace(/[^a-z0-9]/gi, '');
|
||||
connection.query('UPDATE users SET reset_token=?, reset_expire=NOW() + INTERVAL 1 HOUR WHERE id=? LIMIT 1', [resetToken, rows[0].id], err => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
settings.list(['serviceUrl', 'adminEmail'], (err, configItems) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
mailer.sendMail({
|
||||
from: {
|
||||
address: configItems.adminEmail
|
||||
},
|
||||
to: {
|
||||
address: rows[0].email
|
||||
},
|
||||
subject: 'Mailer password change request'
|
||||
}, {
|
||||
template: 'emails/password-reset.hbs',
|
||||
data: {
|
||||
username: rows[0].username,
|
||||
confirmUrl: urllib.resolve(configItems.serviceUrl, '/users/reset') + '?token=' + encodeURIComponent(resetToken) + '&username=' + encodeURIComponent(rows[0].username)
|
||||
}
|
||||
}, err => {
|
||||
if (err) {
|
||||
log.error('Mail', err.stack); // eslint-disable-line no-console
|
||||
}
|
||||
});
|
||||
|
||||
callback(null, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.checkResetToken = (username, resetToken, callback) => {
|
||||
if (!username || !resetToken) {
|
||||
return callback(new Error('Missing username or reset token'));
|
||||
}
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
connection.query('SELECT id FROM users WHERE username=? AND reset_token=? AND reset_expire > NOW() LIMIT 1', [username, resetToken], (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, rows && rows.length || false);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.resetPassword = (data, callback) => {
|
||||
let updates = tools.convertKeys(data);
|
||||
|
||||
if (!updates.username || !updates.resetToken) {
|
||||
return callback(new Error('Missing username or reset token'));
|
||||
}
|
||||
|
||||
if (!updates.password || !updates.password2 || updates.password !== updates.password2) {
|
||||
return callback(new Error('Invalid new password'));
|
||||
}
|
||||
|
||||
bcrypt.hash(updates.password, null, null, (err, hash) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
connection.query('UPDATE users SET password=?, reset_token=NULL, reset_expire=NULL WHERE username=? AND reset_token=? AND reset_expire > NOW() LIMIT 1', [hash, updates.username, updates.resetToken], (err, result) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, result.affectedRows);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue