Work in progress on subscriptions
This commit is contained in:
parent
eecb3cd067
commit
b22a87e712
18 changed files with 1729 additions and 884 deletions
55
app.js
55
app.js
|
@ -226,11 +226,30 @@ app.use((req, res, next) => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Endpoint under /api are authenticated by access token
|
||||||
|
app.all('/api/*', passport.authByPanelToken);
|
||||||
|
|
||||||
|
|
||||||
|
// Marks the following endpoint to return JSON object when error occurs
|
||||||
|
app.all('/api/*', (req, res, next) => {
|
||||||
|
req.needsAPIJSONResponse = true;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.all('/rest/*', (req, res, next) => {
|
||||||
|
req.needsRESTJSONResponse = true;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Initializes the request context to be used for authorization
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
req.context = contextHelpers.getRequestContext(req);
|
req.context = contextHelpers.getRequestContext(req);
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Regular endpoints
|
||||||
app.use('/', routes);
|
app.use('/', routes);
|
||||||
app.use('/lists', lists);
|
app.use('/lists', lists);
|
||||||
app.use('/templates', templates);
|
app.use('/templates', templates);
|
||||||
|
@ -244,12 +263,15 @@ app.use('/triggers', triggers);
|
||||||
app.use('/webhooks', webhooks);
|
app.use('/webhooks', webhooks);
|
||||||
app.use('/subscription', subscription);
|
app.use('/subscription', subscription);
|
||||||
app.use('/archive', archive);
|
app.use('/archive', archive);
|
||||||
app.use('/api', api);
|
|
||||||
app.use('/editorapi', editorapi);
|
app.use('/editorapi', editorapi);
|
||||||
app.use('/grapejs', grapejs);
|
app.use('/grapejs', grapejs);
|
||||||
app.use('/mosaico', mosaico);
|
app.use('/mosaico', mosaico);
|
||||||
|
|
||||||
|
|
||||||
|
// API endpoints
|
||||||
|
app.use('/api', api);
|
||||||
|
|
||||||
|
|
||||||
if (config.reports && config.reports.enabled === true) {
|
if (config.reports && config.reports.enabled === true) {
|
||||||
app.use('/reports', reports);
|
app.use('/reports', reports);
|
||||||
}
|
}
|
||||||
|
@ -267,11 +289,7 @@ if (config.reports && config.reports.enabled === true) {
|
||||||
}
|
}
|
||||||
/* ------------------------------------------------------------------- */
|
/* ------------------------------------------------------------------- */
|
||||||
|
|
||||||
app.all('/rest/*', (req, res, next) => {
|
// REST endpoints
|
||||||
req.needsJSONResponse = true;
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
app.use('/rest', namespacesRest);
|
app.use('/rest', namespacesRest);
|
||||||
app.use('/rest', usersRest);
|
app.use('/rest', usersRest);
|
||||||
app.use('/rest', accountRest);
|
app.use('/rest', accountRest);
|
||||||
|
@ -289,6 +307,7 @@ if (config.reports && config.reports.enabled === true) {
|
||||||
app.use('/rest', reportsRest);
|
app.use('/rest', reportsRest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// catch 404 and forward to error handler
|
// catch 404 and forward to error handler
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
let err = new Error(_('Not Found'));
|
let err = new Error(_('Not Found'));
|
||||||
|
@ -296,8 +315,8 @@ app.use((req, res, next) => {
|
||||||
next(err);
|
next(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
// error handlers
|
|
||||||
|
|
||||||
|
// Error handlers
|
||||||
if (app.get('env') === 'development') {
|
if (app.get('env') === 'development') {
|
||||||
// development error handler
|
// development error handler
|
||||||
// will print stacktrace
|
// will print stacktrace
|
||||||
|
@ -306,7 +325,7 @@ if (app.get('env') === 'development') {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.needsJSONResponse) {
|
if (req.needsRESTJSONResponse) {
|
||||||
const resp = {
|
const resp = {
|
||||||
message: err.message,
|
message: err.message,
|
||||||
error: err
|
error: err
|
||||||
|
@ -319,6 +338,14 @@ if (app.get('env') === 'development') {
|
||||||
|
|
||||||
res.status(err.status || 500).json(resp);
|
res.status(err.status || 500).json(resp);
|
||||||
|
|
||||||
|
} else if (req.needsAPIJSONResponse) {
|
||||||
|
const resp = {
|
||||||
|
error: err.message || err,
|
||||||
|
data: []
|
||||||
|
};
|
||||||
|
|
||||||
|
return status(err.status || 500).json(resp);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
if (err instanceof interoperableErrors.NotLoggedInError) {
|
if (err instanceof interoperableErrors.NotLoggedInError) {
|
||||||
req.flash('danger', _('Need to be logged in to access restricted content'));
|
req.flash('danger', _('Need to be logged in to access restricted content'));
|
||||||
|
@ -342,7 +369,7 @@ if (app.get('env') === 'development') {
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(err);
|
console.log(err);
|
||||||
if (req.needsJSONResponse) {
|
if (req.needsRESTJSONResponse) {
|
||||||
const resp = {
|
const resp = {
|
||||||
message: err.message,
|
message: err.message,
|
||||||
error: {}
|
error: {}
|
||||||
|
@ -355,7 +382,17 @@ if (app.get('env') === 'development') {
|
||||||
|
|
||||||
res.status(err.status || 500).json(resp);
|
res.status(err.status || 500).json(resp);
|
||||||
|
|
||||||
|
} else if (req.needsAPIJSONResponse) {
|
||||||
|
const resp = {
|
||||||
|
error: err.message || err,
|
||||||
|
data: []
|
||||||
|
};
|
||||||
|
|
||||||
|
return status(err.status || 500).json(resp);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
// TODO: Render interoperable errors using a special client that does internationalization of the error message
|
||||||
|
|
||||||
if (err instanceof interoperableErrors.NotLoggedInError) {
|
if (err instanceof interoperableErrors.NotLoggedInError) {
|
||||||
req.flash('danger', _('Need to be logged in to access restricted content'));
|
req.flash('danger', _('Need to be logged in to access restricted content'));
|
||||||
return res.redirect('/account/login?next=' + encodeURIComponent(req.originalUrl));
|
return res.redirect('/account/login?next=' + encodeURIComponent(req.originalUrl));
|
||||||
|
|
176
lib/helpers.js
176
lib/helpers.js
|
@ -18,11 +18,6 @@ module.exports = {
|
||||||
getDefaultMergeTags,
|
getDefaultMergeTags,
|
||||||
getRSSMergeTags,
|
getRSSMergeTags,
|
||||||
getListMergeTags,
|
getListMergeTags,
|
||||||
captureFlashMessages,
|
|
||||||
injectCustomFormData,
|
|
||||||
injectCustomFormTemplates,
|
|
||||||
filterCustomFields,
|
|
||||||
getMjmlTemplate,
|
|
||||||
rollbackAndReleaseConnection,
|
rollbackAndReleaseConnection,
|
||||||
filterObject,
|
filterObject,
|
||||||
enforce
|
enforce
|
||||||
|
@ -119,176 +114,7 @@ function getListMergeTags(listId, callback) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterCustomFields(customFieldsIn = [], fieldIds = [], method = 'include') {
|
// FIXME - remove once we get rid of non-async models
|
||||||
let customFields = customFieldsIn.slice();
|
|
||||||
fieldIds = typeof fieldIds === 'string' ? fieldIds.split(',') : fieldIds;
|
|
||||||
|
|
||||||
customFields.unshift({
|
|
||||||
id: 'email',
|
|
||||||
name: 'Email Address',
|
|
||||||
type: 'Email',
|
|
||||||
typeSubscriptionEmail: true
|
|
||||||
}, {
|
|
||||||
id: 'firstname',
|
|
||||||
name: 'First Name',
|
|
||||||
type: 'Text',
|
|
||||||
typeFirstName: true
|
|
||||||
}, {
|
|
||||||
id: 'lastname',
|
|
||||||
name: 'Last Name',
|
|
||||||
type: 'Text',
|
|
||||||
typeLastName: true
|
|
||||||
});
|
|
||||||
|
|
||||||
let filtered = [];
|
|
||||||
|
|
||||||
if (method === 'include') {
|
|
||||||
fieldIds.forEach(id => {
|
|
||||||
let field = customFields.find(f => f.id.toString() === id);
|
|
||||||
field && filtered.push(field);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
customFields.forEach(field => {
|
|
||||||
!fieldIds.includes(field.id.toString()) && filtered.push(field);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return filtered;
|
|
||||||
}
|
|
||||||
|
|
||||||
function injectCustomFormData(customFormId, viewPath, data, callback) {
|
|
||||||
|
|
||||||
let injectDefaultData = data => {
|
|
||||||
data.customFields = filterCustomFields(data.customFields, [], 'exclude');
|
|
||||||
data.formInputStyle = '@import url(/subscription/form-input-style.css);';
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (Number(customFormId) < 1) {
|
|
||||||
return callback(null, injectDefaultData(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
forms.get(customFormId, (err, form) => {
|
|
||||||
if (err) {
|
|
||||||
return callback(null, injectDefaultData(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
let view = viewPath.split('/')[1];
|
|
||||||
|
|
||||||
if (view === 'web-subscribe') {
|
|
||||||
data.customFields = form.fieldsShownOnSubscribe
|
|
||||||
? filterCustomFields(data.customFields, form.fieldsShownOnSubscribe)
|
|
||||||
: filterCustomFields(data.customFields, [], 'exclude');
|
|
||||||
} else if (view === 'web-manage') {
|
|
||||||
data.customFields = form.fieldsShownOnManage
|
|
||||||
? filterCustomFields(data.customFields, form.fieldsShownOnManage)
|
|
||||||
: filterCustomFields(data.customFields, [], 'exclude');
|
|
||||||
}
|
|
||||||
|
|
||||||
let key = tools.fromDbKey(view);
|
|
||||||
data.template.template = form[key] || data.template.template;
|
|
||||||
data.template.layout = form.layout || data.template.layout;
|
|
||||||
data.formInputStyle = form.formInputStyle || '@import url(/subscription/form-input-style.css);';
|
|
||||||
|
|
||||||
settings.list(['ua_code'], (err, configItems) => {
|
|
||||||
if (err) {
|
|
||||||
return callback(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
data.uaCode = configItems.uaCode;
|
|
||||||
data.customSubscriptionScripts = config.customsubscriptionscripts || [];
|
|
||||||
callback(null, data);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function injectCustomFormTemplates(customFormId, templates, callback) {
|
|
||||||
if (Number(customFormId) < 1) {
|
|
||||||
return callback(null, templates);
|
|
||||||
}
|
|
||||||
|
|
||||||
forms.get(customFormId, (err, form) => {
|
|
||||||
if (err) {
|
|
||||||
return callback(null, templates);
|
|
||||||
}
|
|
||||||
|
|
||||||
let lookUp = name => {
|
|
||||||
let key = tools.fromDbKey(
|
|
||||||
/subscription\/([^.]*)/.exec(name)[1]
|
|
||||||
);
|
|
||||||
return form[key] || name;
|
|
||||||
};
|
|
||||||
|
|
||||||
Object.keys(templates).forEach(key => {
|
|
||||||
let value = templates[key];
|
|
||||||
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
templates[key] = lookUp(value);
|
|
||||||
}
|
|
||||||
if (typeof value === 'object' && value.template) {
|
|
||||||
templates[key].template = lookUp(value.template);
|
|
||||||
}
|
|
||||||
if (typeof value === 'object' && value.layout) {
|
|
||||||
templates[key].layout = lookUp(value.layout);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
callback(null, templates);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMjmlTemplate(template, callback) {
|
|
||||||
if (!template) {
|
|
||||||
return callback(null, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
let key = (typeof template === 'object') ? objectHash(template) : template;
|
|
||||||
|
|
||||||
if (mjmlTemplates.has(key)) {
|
|
||||||
return callback(null, mjmlTemplates.get(key));
|
|
||||||
}
|
|
||||||
|
|
||||||
let done = source => {
|
|
||||||
let compiled;
|
|
||||||
try {
|
|
||||||
compiled = mjml.mjml2html(source);
|
|
||||||
} catch (err) {
|
|
||||||
return callback(err);
|
|
||||||
}
|
|
||||||
if (compiled.errors.length) {
|
|
||||||
return callback(compiled.errors[0].message || compiled.errors[0]);
|
|
||||||
}
|
|
||||||
let renderer = hbs.handlebars.compile(compiled.html);
|
|
||||||
mjmlTemplates.set(key, renderer);
|
|
||||||
callback(null, renderer);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (typeof template === 'object') {
|
|
||||||
tools.mergeTemplateIntoLayout(template.template, template.layout, (err, source) => {
|
|
||||||
if (err) {
|
|
||||||
return callback(err);
|
|
||||||
}
|
|
||||||
done(source);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
fs.readFile(path.join(__dirname, '..', 'views', template), 'utf-8', (err, source) => {
|
|
||||||
if (err) {
|
|
||||||
return callback(err);
|
|
||||||
}
|
|
||||||
done(source);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function captureFlashMessages(req, res, callback) {
|
|
||||||
res.render('subscription/capture-flash-messages', { layout: null }, (err, flash) => {
|
|
||||||
if (err) {
|
|
||||||
return callback(err);
|
|
||||||
}
|
|
||||||
callback(null, flash);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function rollbackAndReleaseConnection(connection, callback) {
|
function rollbackAndReleaseConnection(connection, callback) {
|
||||||
connection.rollback(() => {
|
connection.rollback(() => {
|
||||||
connection.release();
|
connection.release();
|
||||||
|
|
|
@ -42,6 +42,38 @@ module.exports.loggedIn = (req, res, next) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
module.exports.authByAccessToken = (req, res, next) => {
|
||||||
|
nodeifyPromise((async () => {
|
||||||
|
if (!req.query.access_token) {
|
||||||
|
res.status(403);
|
||||||
|
return res.json({
|
||||||
|
error: 'Missing access_token',
|
||||||
|
data: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await users.getByAccessToken(req.query.access_token);
|
||||||
|
req.user = user;
|
||||||
|
next();
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof interoperableErrors.NotFoundError) {
|
||||||
|
res.status(403);
|
||||||
|
return res.json({
|
||||||
|
error: 'Invalid or expired access_token',
|
||||||
|
data: []
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.status(500);
|
||||||
|
return res.json({
|
||||||
|
error: err.message || err,
|
||||||
|
data: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})(), next);
|
||||||
|
};
|
||||||
|
|
||||||
module.exports.setup = app => {
|
module.exports.setup = app => {
|
||||||
app.use(passport.initialize());
|
app.use(passport.initialize());
|
||||||
app.use(passport.session());
|
app.use(passport.session());
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const log = require('npmlog');
|
const log = require('npmlog');
|
||||||
let fields = require('./models/fields');
|
const fields = require('../models/fields');
|
||||||
let settings = require('./models/settings');
|
const settings = require('../models/settings');
|
||||||
let mailer = require('./mailer');
|
const urllib = require('url');
|
||||||
let urllib = require('url');
|
const helpers = require('./helpers');
|
||||||
let helpers = require('./helpers');
|
const _ = require('./translate')._;
|
||||||
let _ = require('./translate')._;
|
const util = require('util');
|
||||||
let util = require('util');
|
const contextHelpers = require('./context-helpers');
|
||||||
|
const {getFieldKey} = require('../shared/lists');
|
||||||
|
const forms = require('../models/forms');
|
||||||
|
const bluebird = require('bluebird');
|
||||||
|
const sendMail = bluebird.promisify(require('./mailer').sendMail);
|
||||||
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
@ -19,16 +23,16 @@ module.exports = {
|
||||||
sendUnsubscriptionConfirmed
|
sendUnsubscriptionConfirmed
|
||||||
};
|
};
|
||||||
|
|
||||||
function sendSubscriptionConfirmed(list, email, subscription, callback) {
|
async function sendSubscriptionConfirmed(list, email, subscription) {
|
||||||
const relativeUrls = {
|
const relativeUrls = {
|
||||||
preferencesUrl: '/subscription/' + list.cid + '/manage/' + subscription.cid,
|
preferencesUrl: '/subscription/' + list.cid + '/manage/' + subscription.cid,
|
||||||
unsubscribeUrl: '/subscription/' + list.cid + '/unsubscribe/' + subscription.cid
|
unsubscribeUrl: '/subscription/' + list.cid + '/unsubscribe/' + subscription.cid
|
||||||
};
|
};
|
||||||
|
|
||||||
sendMail(list, email, 'subscription-confirmed', _('%s: Subscription Confirmed'), relativeUrls, {}, subscription, callback);
|
await sendMail(list, email, 'subscription_confirmed', _('%s: Subscription Confirmed'), relativeUrls, {}, subscription);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendAlreadySubscribed(list, email, subscription, callback) {
|
async function sendAlreadySubscribed(list, email, subscription) {
|
||||||
const mailOpts = {
|
const mailOpts = {
|
||||||
ignoreDisableConfirmations: true
|
ignoreDisableConfirmations: true
|
||||||
};
|
};
|
||||||
|
@ -36,69 +40,93 @@ function sendAlreadySubscribed(list, email, subscription, callback) {
|
||||||
preferencesUrl: '/subscription/' + list.cid + '/manage/' + subscription.cid,
|
preferencesUrl: '/subscription/' + list.cid + '/manage/' + subscription.cid,
|
||||||
unsubscribeUrl: '/subscription/' + list.cid + '/unsubscribe/' + subscription.cid
|
unsubscribeUrl: '/subscription/' + list.cid + '/unsubscribe/' + subscription.cid
|
||||||
};
|
};
|
||||||
sendMail(list, email, 'already-subscribed', _('%s: Email Address Already Registered'), relativeUrls, mailOpts, subscription, callback);
|
await sendMail(list, email, 'already_subscribed', _('%s: Email Address Already Registered'), relativeUrls, mailOpts, subscription);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendConfirmAddressChange(list, email, cid, subscription, callback) {
|
async function sendConfirmAddressChange(list, email, cid, subscription) {
|
||||||
const mailOpts = {
|
const mailOpts = {
|
||||||
ignoreDisableConfirmations: true
|
ignoreDisableConfirmations: true
|
||||||
};
|
};
|
||||||
const relativeUrls = {
|
const relativeUrls = {
|
||||||
confirmUrl: '/subscription/confirm/change-address/' + cid
|
confirmUrl: '/subscription/confirm/change-address/' + cid
|
||||||
};
|
};
|
||||||
sendMail(list, email, 'confirm-address-change', _('%s: Please Confirm Email Change in Subscription'), relativeUrls, mailOpts, subscription, callback);
|
await sendMail(list, email, 'confirm_address_change', _('%s: Please Confirm Email Change in Subscription'), relativeUrls, mailOpts, subscription);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendConfirmSubscription(list, email, cid, subscription, callback) {
|
async function sendConfirmSubscription(list, email, cid, subscription) {
|
||||||
const mailOpts = {
|
const mailOpts = {
|
||||||
ignoreDisableConfirmations: true
|
ignoreDisableConfirmations: true
|
||||||
};
|
};
|
||||||
const relativeUrls = {
|
const relativeUrls = {
|
||||||
confirmUrl: '/subscription/confirm/subscribe/' + cid
|
confirmUrl: '/subscription/confirm/subscribe/' + cid
|
||||||
};
|
};
|
||||||
sendMail(list, email, 'confirm-subscription', _('%s: Please Confirm Subscription'), relativeUrls, mailOpts, subscription, callback);
|
await sendMail(list, email, 'confirm_subscription', _('%s: Please Confirm Subscription'), relativeUrls, mailOpts, subscription);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendConfirmUnsubscription(list, email, cid, subscription, callback) {
|
async function sendConfirmUnsubscription(list, email, cid, subscription) {
|
||||||
const mailOpts = {
|
const mailOpts = {
|
||||||
ignoreDisableConfirmations: true
|
ignoreDisableConfirmations: true
|
||||||
};
|
};
|
||||||
const relativeUrls = {
|
const relativeUrls = {
|
||||||
confirmUrl: '/subscription/confirm/unsubscribe/' + cid
|
confirmUrl: '/subscription/confirm/unsubscribe/' + cid
|
||||||
};
|
};
|
||||||
sendMail(list, email, 'confirm-unsubscription', _('%s: Please Confirm Unsubscription'), relativeUrls, mailOpts, subscription, callback);
|
await sendMail(list, email, 'confirm_unsubscription', _('%s: Please Confirm Unsubscription'), relativeUrls, mailOpts, subscription);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendUnsubscriptionConfirmed(list, email, subscription, callback) {
|
async function sendUnsubscriptionConfirmed(list, email, subscription) {
|
||||||
const relativeUrls = {
|
const relativeUrls = {
|
||||||
subscribeUrl: '/subscription/' + list.cid + '?cid=' + subscription.cid
|
subscribeUrl: '/subscription/' + list.cid + '?cid=' + subscription.cid
|
||||||
};
|
};
|
||||||
sendMail(list, email, 'unsubscription-confirmed', _('%s: Unsubscription Confirmed'), relativeUrls, {}, subscription, callback);
|
await sendMail(list, email, 'unsubscription_confirmed', _('%s: Unsubscription Confirmed'), relativeUrls, {}, subscription);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDisplayName(flds, subscription) {
|
||||||
|
let firstName, lastName, name;
|
||||||
|
|
||||||
function sendMail(list, email, template, subject, relativeUrls, mailOpts, subscription, callback) {
|
for (const fld of flds) {
|
||||||
fields.list(list.id, (err, fieldList) => {
|
if (fld.key === 'FIRST_NAME') {
|
||||||
if (err) {
|
firstName = subscription[fld.column];
|
||||||
return callback(err);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let encryptionKeys = [];
|
if (fld.key === 'LAST_NAME') {
|
||||||
fields.getRow(fieldList, subscription).forEach(field => {
|
lastName = subscription[fld.column];
|
||||||
if (field.type === 'gpg' && field.value) {
|
|
||||||
encryptionKeys.push(field.value.trim());
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
settings.list(['defaultHomepage', 'defaultFrom', 'defaultAddress', 'defaultPostaddress', 'serviceUrl', 'disableConfirmations'], (err, configItems) => {
|
if (fld.key === 'NAME') {
|
||||||
if (err) {
|
name = subscription[fld.column];
|
||||||
return callback(err);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name) {
|
||||||
|
return name;
|
||||||
|
} else if (firstName && lastName) {
|
||||||
|
return firstName + ' ' + lastName;
|
||||||
|
} else if (lastName) {
|
||||||
|
return lastName;
|
||||||
|
} else if (firstName) {
|
||||||
|
return firstName;
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendMail(list, email, template, subject, relativeUrls, mailOpts, subscription) {
|
||||||
|
console.log(subscription);
|
||||||
|
|
||||||
|
const flds = await fields.list(contextHelpers.getAdminContext(), list.id);
|
||||||
|
|
||||||
|
const encryptionKeys = [];
|
||||||
|
for (const fld of flds) {
|
||||||
|
if (fld.type === 'gpg' && field.value) {
|
||||||
|
encryptionKeys.push(subscription[getFieldKey(fld)].value.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const configItems = await settings.get(['defaultHomepage', 'defaultFrom', 'defaultAddress', 'defaultPostaddress', 'serviceUrl', 'disableConfirmations']);
|
||||||
|
|
||||||
if (!mailOpts.ignoreDisableConfirmations && configItems.disableConfirmations) {
|
if (!mailOpts.ignoreDisableConfirmations && configItems.disableConfirmations) {
|
||||||
return callback();
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
title: list.name,
|
title: list.name,
|
||||||
homepage: configItems.defaultHomepage || configItems.serviceUrl,
|
homepage: configItems.defaultHomepage || configItems.serviceUrl,
|
||||||
|
@ -110,29 +138,33 @@ function sendMail(list, email, template, subject, relativeUrls, mailOpts, subscr
|
||||||
data[relativeUrlKey] = urllib.resolve(configItems.serviceUrl, relativeUrls[relativeUrlKey]);
|
data[relativeUrlKey] = urllib.resolve(configItems.serviceUrl, relativeUrls[relativeUrlKey]);
|
||||||
}
|
}
|
||||||
|
|
||||||
let text = {
|
const fsTemplate = template.replace(/_/g, '-');
|
||||||
template: 'subscription/mail-' + template + '-text.hbs'
|
const text = {
|
||||||
|
template: 'subscription/mail-' + fsTemplate + '-text.hbs'
|
||||||
};
|
};
|
||||||
|
|
||||||
let html = {
|
const html = {
|
||||||
template: 'subscription/mail-' + template + '-html.mjml.hbs',
|
template: 'subscription/mail-' + fsTemplate + '-html.mjml.hbs',
|
||||||
layout: 'subscription/layout.mjml.hbs',
|
layout: 'subscription/layout.mjml.hbs',
|
||||||
type: 'mjml'
|
type: 'mjml'
|
||||||
};
|
};
|
||||||
|
|
||||||
helpers.injectCustomFormTemplates(list.defaultForm, { text, html }, (err, tmpl) => {
|
if (list.default_form !== null) {
|
||||||
if (!err && tmpl) {
|
const form = await forms.getById(contextHelpers.getAdminContext(), list.default_form);
|
||||||
text = tmpl.text || text;
|
|
||||||
html = tmpl.html || html;
|
text.template = form['mail_' + template + '_text'] || text.template;
|
||||||
|
html.template = form['mail_' + template + '_html'] || html.template;
|
||||||
|
html.layout = form.layout || html.layout;
|
||||||
}
|
}
|
||||||
|
|
||||||
mailer.sendMail({
|
try {
|
||||||
|
await sendMail({
|
||||||
from: {
|
from: {
|
||||||
name: configItems.defaultFrom,
|
name: configItems.defaultFrom,
|
||||||
address: configItems.defaultAddress
|
address: configItems.defaultAddress
|
||||||
},
|
},
|
||||||
to: {
|
to: {
|
||||||
name: [].concat(subscription.firstName || []).concat(subscription.lastName || []).join(' '),
|
name: getDisplayName(flds, subscription),
|
||||||
address: email
|
address: email
|
||||||
},
|
},
|
||||||
subject: util.format(subject, list.name),
|
subject: util.format(subject, list.name),
|
||||||
|
@ -141,15 +173,8 @@ function sendMail(list, email, template, subject, relativeUrls, mailOpts, subscr
|
||||||
html,
|
html,
|
||||||
text,
|
text,
|
||||||
data
|
data
|
||||||
}, err => {
|
});
|
||||||
if (err) {
|
} catch (err) {
|
||||||
log.error('Subscription', err);
|
log.error('Subscription', err);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
callback();
|
|
||||||
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,12 @@ const _ = require('./translate')._;
|
||||||
const util = require('util');
|
const util = require('util');
|
||||||
const isemail = require('isemail');
|
const isemail = require('isemail');
|
||||||
|
|
||||||
|
const bluebird = require('bluebird');
|
||||||
|
const mergeTemplateIntoLayout = bluebird.promisify(require('./tools').mergeTemplateIntoLayout);
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
validateEmail
|
validateEmail,
|
||||||
|
mergeTemplateIntoLayout
|
||||||
};
|
};
|
||||||
|
|
||||||
async function validateEmail(address, checkBlocked) {
|
async function validateEmail(address, checkBlocked) {
|
||||||
|
@ -24,3 +28,4 @@ async function validateEmail(address, checkBlocked) {
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
41
lib/tools.js
41
lib/tools.js
|
@ -18,9 +18,6 @@ let htmlToText = require('html-to-text');
|
||||||
let blockedUsers = ['abuse', 'admin', 'billing', 'compliance', 'devnull', 'dns', 'ftp', 'hostmaster', 'inoc', 'ispfeedback', 'ispsupport', 'listrequest', 'list', 'maildaemon', 'noc', 'noreply', 'noreply', 'null', 'phish', 'phishing', 'postmaster', 'privacy', 'registrar', 'root', 'security', 'spam', 'support', 'sysadmin', 'tech', 'undisclosedrecipients', 'unsubscribe', 'usenet', 'uucp', 'webmaster', 'www'];
|
let blockedUsers = ['abuse', 'admin', 'billing', 'compliance', 'devnull', 'dns', 'ftp', 'hostmaster', 'inoc', 'ispfeedback', 'ispsupport', 'listrequest', 'list', 'maildaemon', 'noc', 'noreply', 'noreply', 'null', 'phish', 'phishing', 'postmaster', 'privacy', 'registrar', 'root', 'security', 'spam', 'support', 'sysadmin', 'tech', 'undisclosedrecipients', 'unsubscribe', 'usenet', 'uucp', 'webmaster', 'www'];
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
toDbKey,
|
|
||||||
fromDbKey,
|
|
||||||
convertKeys,
|
|
||||||
queryParams,
|
queryParams,
|
||||||
createSlug,
|
createSlug,
|
||||||
updateMenu,
|
updateMenu,
|
||||||
|
@ -33,42 +30,6 @@ module.exports = {
|
||||||
workers: new Set()
|
workers: new Set()
|
||||||
};
|
};
|
||||||
|
|
||||||
function toDbKey(key) {
|
|
||||||
return key.
|
|
||||||
replace(/[^a-z0-9\-_]/gi, '').
|
|
||||||
replace(/-+/g, '_').
|
|
||||||
replace(/[A-Z]/g, c => '_' + c.toLowerCase()).
|
|
||||||
replace(/^_+|_+$/g, '').
|
|
||||||
replace(/_+/g, '_').
|
|
||||||
trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
function fromDbKey(key) {
|
|
||||||
let prefix = '';
|
|
||||||
if (key.startsWith('_')) {
|
|
||||||
key = key.substring(1);
|
|
||||||
prefix = '_';
|
|
||||||
|
|
||||||
}
|
|
||||||
return prefix + key.replace(/[_-]([a-z])/g, (m, c) => c.toUpperCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
function convertKeys(obj, options) {
|
|
||||||
options = options || {};
|
|
||||||
let response = {};
|
|
||||||
Object.keys(obj || {}).forEach(key => {
|
|
||||||
let lKey = fromDbKey(key);
|
|
||||||
if (options.skip && options.skip.indexOf(lKey) >= 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (options.keep && options.keep.indexOf(lKey) < 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
response[lKey] = obj[key];
|
|
||||||
});
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
function queryParams(obj) {
|
function queryParams(obj) {
|
||||||
return Object.keys(obj).
|
return Object.keys(obj).
|
||||||
filter(key => key !== '_csrf').
|
filter(key => key !== '_csrf').
|
||||||
|
@ -116,6 +77,7 @@ function createSlug(table, name, callback) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME - remove once we fully manage the menu in the client
|
||||||
function updateMenu(res) {
|
function updateMenu(res) {
|
||||||
if (!res.locals.menu) {
|
if (!res.locals.menu) {
|
||||||
res.locals.menu = [];
|
res.locals.menu = [];
|
||||||
|
@ -148,6 +110,7 @@ function updateMenu(res) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME - either remove of delegate to validateEmail in tools-async (or vice-versa)
|
||||||
function validateEmail(address, checkBlocked, callback) {
|
function validateEmail(address, checkBlocked, callback) {
|
||||||
let user = (address || '').toString().split('@').shift().toLowerCase().replace(/[^a-z0-9]/g, '');
|
let user = (address || '').toString().split('@').shift().toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||||
if (checkBlocked && blockedUsers.indexOf(user) >= 0) {
|
if (checkBlocked && blockedUsers.indexOf(user) >= 0) {
|
||||||
|
|
|
@ -16,6 +16,53 @@ async function listDTAjax(context, params) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
module.exports.get = (start, limit, search, callback) => {
|
||||||
|
db.getConnection((err, connection) => {
|
||||||
|
if (err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
search = '%' + search + '%';
|
||||||
|
connection.query('SELECT SQL_CALC_FOUND_ROWS `email` FROM blacklist WHERE `email` LIKE ? ORDER BY `email` LIMIT ? OFFSET ?', [search, limit, start], (err, rows) => {
|
||||||
|
if (err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.query('SELECT FOUND_ROWS() AS total', (err, total) => {
|
||||||
|
connection.release();
|
||||||
|
if (err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
let emails = [];
|
||||||
|
rows.forEach(email => {
|
||||||
|
emails.push(email.email);
|
||||||
|
});
|
||||||
|
return callback(null, emails, total && total[0] && total[0].total);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
|
async function search(context, start, limit, search) {
|
||||||
|
return await knex.transaction(async tx => {
|
||||||
|
shares.enforceGlobalPermission(context, 'manageBlacklist');
|
||||||
|
|
||||||
|
search = '%' + search + '%';
|
||||||
|
|
||||||
|
const count = await tx('blacklist').where('email', 'like', search).count();
|
||||||
|
// FIXME - the count won't likely work;
|
||||||
|
console.log(count);
|
||||||
|
|
||||||
|
const rows = await tx('blacklist').where('email', 'like', search).offset(start).limit(limit);
|
||||||
|
|
||||||
|
return {
|
||||||
|
emails: rows.map(row => row.email),
|
||||||
|
total: count
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function add(context, email) {
|
async function add(context, email) {
|
||||||
return await knex.transaction(async tx => {
|
return await knex.transaction(async tx => {
|
||||||
shares.enforceGlobalPermission(context, 'manageBlacklist');
|
shares.enforceGlobalPermission(context, 'manageBlacklist');
|
||||||
|
@ -56,6 +103,7 @@ module.exports = {
|
||||||
listDTAjax,
|
listDTAjax,
|
||||||
add,
|
add,
|
||||||
remove,
|
remove,
|
||||||
|
search,
|
||||||
isBlacklisted,
|
isBlacklisted,
|
||||||
serverValidate
|
serverValidate
|
||||||
};
|
};
|
51
models/confirmations.js
Normal file
51
models/confirmations.js
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const knex = require('../lib/knex');
|
||||||
|
const shortid = require('shortid');
|
||||||
|
|
||||||
|
async function addConfirmation(listId, action, ip, data) {
|
||||||
|
const cid = shortid.generate();
|
||||||
|
await knex('confirmations').insert({
|
||||||
|
cid,
|
||||||
|
list: listId,
|
||||||
|
action,
|
||||||
|
ip,
|
||||||
|
data: JSON.stringify(data || {})
|
||||||
|
});
|
||||||
|
|
||||||
|
return cid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Atomically retrieves confirmation from the database, removes it from the database and returns it.
|
||||||
|
*/
|
||||||
|
async function takeConfirmation(cid) {
|
||||||
|
return await knex.transaction(async tx => {
|
||||||
|
const entry = await tx('confirmations').select(['cid', 'list', 'action', 'ip', 'data']).where('cid', cid).first();
|
||||||
|
|
||||||
|
if (!entry) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await tx('confirmations').where('cid', cid).del();
|
||||||
|
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
data = JSON.parse(entry.data);
|
||||||
|
} catch (err) {
|
||||||
|
data = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
list: entry.list,
|
||||||
|
action: entry.action,
|
||||||
|
ip: entry.ip,
|
||||||
|
data
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
addConfirmation,
|
||||||
|
takeConfirmation
|
||||||
|
};
|
|
@ -7,7 +7,6 @@ const { enforce, filterObject } = require('../lib/helpers');
|
||||||
const dtHelpers = require('../lib/dt-helpers');
|
const dtHelpers = require('../lib/dt-helpers');
|
||||||
const interoperableErrors = require('../shared/interoperable-errors');
|
const interoperableErrors = require('../shared/interoperable-errors');
|
||||||
const shares = require('./shares');
|
const shares = require('./shares');
|
||||||
const bluebird = require('bluebird');
|
|
||||||
const validators = require('../shared/validators');
|
const validators = require('../shared/validators');
|
||||||
const shortid = require('shortid');
|
const shortid = require('shortid');
|
||||||
const segments = require('./segments');
|
const segments = require('./segments');
|
||||||
|
@ -154,7 +153,7 @@ async function listTx(tx, listId) {
|
||||||
|
|
||||||
async function list(context, listId) {
|
async function list(context, listId) {
|
||||||
return await knex.transaction(async tx => {
|
return await knex.transaction(async tx => {
|
||||||
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, ['manageFields', 'manageSegments']);
|
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, ['viewSubscriptions', 'manageFields', 'manageSegments']);
|
||||||
return await listTx(tx, listId);
|
return await listTx(tx, listId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -193,6 +192,7 @@ async function listGroupedTx(tx, listId) {
|
||||||
|
|
||||||
async function listGrouped(context, listId) {
|
async function listGrouped(context, listId) {
|
||||||
return await knex.transaction(async tx => {
|
return await knex.transaction(async tx => {
|
||||||
|
// It may seem odd why there is not 'manageFields' here. But it's just a result of strictly apply the "need-to-know" principle. Simply, at this point this function is needed only in managing subscriptions.
|
||||||
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, ['manageSubscriptions']);
|
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, ['manageSubscriptions']);
|
||||||
return await listGroupedTx(tx, listId);
|
return await listGroupedTx(tx, listId);
|
||||||
});
|
});
|
||||||
|
@ -474,6 +474,32 @@ async function removeAllByListIdTx(tx, context, listId) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getRow(context, listId, subscription) {
|
||||||
|
const customFields = [{
|
||||||
|
name: 'Email Address',
|
||||||
|
column: 'email',
|
||||||
|
typeSubscriptionEmail: true,
|
||||||
|
value: subscription ? subscription.email : '',
|
||||||
|
order_subscribe: -1,
|
||||||
|
order_manage: -1
|
||||||
|
}];
|
||||||
|
|
||||||
|
const flds = await list(context, listId);
|
||||||
|
|
||||||
|
for (const fld of flds) {
|
||||||
|
if (fld.column) {
|
||||||
|
customFields.push({
|
||||||
|
name: fld.name,
|
||||||
|
column: fld.column,
|
||||||
|
['type' + fld.type.replace(/(?:^|-)([a-z])/g, (m, c) => c.toUpperCase())]: true,
|
||||||
|
value: subscription ? subscription[fld.column] : ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return customFields;
|
||||||
|
}
|
||||||
|
|
||||||
// This is to handle circular dependency with segments.js
|
// This is to handle circular dependency with segments.js
|
||||||
Object.assign(module.exports, {
|
Object.assign(module.exports, {
|
||||||
Cardinality,
|
Cardinality,
|
||||||
|
@ -491,5 +517,6 @@ Object.assign(module.exports, {
|
||||||
updateWithConsistencyCheck,
|
updateWithConsistencyCheck,
|
||||||
remove,
|
remove,
|
||||||
removeAllByListIdTx,
|
removeAllByListIdTx,
|
||||||
serverValidate
|
serverValidate,
|
||||||
|
getRow
|
||||||
});
|
});
|
|
@ -32,16 +32,40 @@ async function listDTAjax(context, params) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getById(context, id) {
|
async function _getByIdTx(tx, context, id) {
|
||||||
return await knex.transaction(async tx => {
|
|
||||||
shares.enforceEntityPermissionTx(tx, context, 'list', id, 'view');
|
shares.enforceEntityPermissionTx(tx, context, 'list', id, 'view');
|
||||||
const entity = await tx('lists').where('id', id).first();
|
const entity = await tx('lists').where('id', id).first();
|
||||||
entity.permissions = await shares.getPermissionsTx(tx, context, 'list', id);
|
entity.permissions = await shares.getPermissionsTx(tx, context, 'list', id);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getById(context, id) {
|
||||||
|
return await knex.transaction(async tx => {
|
||||||
|
return _getByIdTx(tx, context, id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getByIdWithListFields(context, id) {
|
||||||
|
return await knex.transaction(async tx => {
|
||||||
|
const entity = _getByIdTx(tx, context, id);
|
||||||
entity.listFields = await fields.listByOrderListTx(tx, id);
|
entity.listFields = await fields.listByOrderListTx(tx, id);
|
||||||
return entity;
|
return entity;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getByCid(context, cid) {
|
||||||
|
return await knex.transaction(async tx => {
|
||||||
|
const entity = await tx('lists').where('cid', cid).first();
|
||||||
|
if (!entity) {
|
||||||
|
shares.throwPermissionDenied();
|
||||||
|
}
|
||||||
|
|
||||||
|
shares.enforceEntityPermissionTx(tx, context, 'list', entity.id, 'view');
|
||||||
|
entity.permissions = await shares.getPermissionsTx(tx, context, 'list', entity.id);
|
||||||
|
return entity;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function create(context, entity) {
|
async function create(context, entity) {
|
||||||
return await knex.transaction(async tx => {
|
return await knex.transaction(async tx => {
|
||||||
await shares.enforceEntityPermissionTx(tx, context, 'namespace', entity.namespace, 'createList');
|
await shares.enforceEntityPermissionTx(tx, context, 'namespace', entity.namespace, 'createList');
|
||||||
|
@ -116,6 +140,8 @@ module.exports = {
|
||||||
hash,
|
hash,
|
||||||
listDTAjax,
|
listDTAjax,
|
||||||
getById,
|
getById,
|
||||||
|
getByIdWithListFields,
|
||||||
|
getByCid,
|
||||||
create,
|
create,
|
||||||
updateWithConsistencyCheck,
|
updateWithConsistencyCheck,
|
||||||
remove,
|
remove,
|
||||||
|
|
|
@ -157,14 +157,14 @@ const campaignFieldsMapping = {
|
||||||
};
|
};
|
||||||
|
|
||||||
async function getCampaignResults(context, campaign, select, extra) {
|
async function getCampaignResults(context, campaign, select, extra) {
|
||||||
const fieldList = await fields.list(context, campaign.list);
|
const flds = await fields.list(context, campaign.list);
|
||||||
|
|
||||||
const fieldsMapping = Object.assign({}, campaignFieldsMapping);
|
const fieldsMapping = Object.assign({}, campaignFieldsMapping);
|
||||||
for (const field of fieldList) {
|
for (const fld of flds) {
|
||||||
/* Dropdowns and checkboxes are aggregated. As such, they have field.column == null
|
/* Dropdown and checkbox groups have field.column == null
|
||||||
TODO - For the time being, we ignore groupped fields. */
|
TODO - For the time being, we don't group options and we don't expand enums. We just provide it as it is in the DB. */
|
||||||
if (field.column) {
|
if (fld.column) {
|
||||||
fieldsMapping[field.key.toLowerCase()] = 'subscribers.' + field.column;
|
fieldsMapping[fld.key.toLowerCase()] = 'subscribers.' + fld.column;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -191,11 +191,11 @@ async function hashByList(listId, entity) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getById(context, listId, id) {
|
async function _getBy(context, listId, key, value) {
|
||||||
return await knex.transaction(async tx => {
|
return await knex.transaction(async tx => {
|
||||||
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'viewSubscriptions');
|
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'viewSubscriptions');
|
||||||
|
|
||||||
const entity = await tx(getTableName(listId)).where('id', id).first();
|
const entity = await tx(getTableName(listId)).where(key, value).first();
|
||||||
|
|
||||||
const groupedFieldsMap = await getGroupedFieldsMap(tx, listId);
|
const groupedFieldsMap = await getGroupedFieldsMap(tx, listId);
|
||||||
groupSubscription(groupedFieldsMap, entity);
|
groupSubscription(groupedFieldsMap, entity);
|
||||||
|
@ -204,6 +204,19 @@ async function getById(context, listId, id) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function getById(context, listId, id) {
|
||||||
|
return await _getBy(context, listId, 'id', id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getByEmail(context, listId, email) {
|
||||||
|
return await _getBy(context, listId, 'email', email);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getByCid(context, listId, cid) {
|
||||||
|
return await _getBy(context, listId, 'cid', cid);
|
||||||
|
}
|
||||||
|
|
||||||
async function listDTAjax(context, listId, segmentId, params) {
|
async function listDTAjax(context, listId, segmentId, params) {
|
||||||
return await knex.transaction(async tx => {
|
return await knex.transaction(async tx => {
|
||||||
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'viewSubscriptions');
|
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'viewSubscriptions');
|
||||||
|
@ -369,7 +382,7 @@ async function _validateAndPreprocess(tx, listId, groupedFieldsMap, entity, isCr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function create(context, listId, entity) {
|
async function create(context, listId, entity, meta = {}) {
|
||||||
return await knex.transaction(async tx => {
|
return await knex.transaction(async tx => {
|
||||||
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSubscriptions');
|
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSubscriptions');
|
||||||
|
|
||||||
|
@ -384,10 +397,9 @@ async function create(context, listId, entity) {
|
||||||
|
|
||||||
ungroupSubscription(groupedFieldsMap, filteredEntity);
|
ungroupSubscription(groupedFieldsMap, filteredEntity);
|
||||||
|
|
||||||
// FIXME - process:
|
filteredEntity.opt_in_ip = meta.ip;
|
||||||
// filteredEntity.opt_in_ip =
|
filteredEntity.opt_in_country = meta.country;
|
||||||
// filteredEntity.opt_in_country =
|
filteredEntity.imported = meta.imported || false;
|
||||||
// filteredEntity.imported =
|
|
||||||
|
|
||||||
const ids = await tx(getTableName(listId)).insert(filteredEntity);
|
const ids = await tx(getTableName(listId)).insert(filteredEntity);
|
||||||
const id = ids[0];
|
const id = ids[0];
|
||||||
|
@ -466,35 +478,58 @@ async function remove(context, listId, id) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function unsubscribe(context, listId, id) {
|
async function unsubscribeAndGet(context, listId, subscriptionId) {
|
||||||
await knex.transaction(async tx => {
|
return await knex.transaction(async tx => {
|
||||||
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSubscriptions');
|
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSubscriptions');
|
||||||
|
|
||||||
const existing = await tx(getTableName(listId)).where('id', id).first();
|
const existing = await tx(getTableName(listId)).where('id', subscriptionId).first();
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
throw new interoperableErrors.NotFoundError();
|
throw new interoperableErrors.NotFoundError();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existing.status === SubscriptionStatus.SUBSCRIBED) {
|
if (existing.status === SubscriptionStatus.SUBSCRIBED) {
|
||||||
await tx(getTableName(listId)).where('id', id).update({
|
existing.status = SubscriptionStatus.UNSUBSCRIBED;
|
||||||
|
|
||||||
|
await tx(getTableName(listId)).where('id', subscriptionId).update({
|
||||||
status: SubscriptionStatus.UNSUBSCRIBED
|
status: SubscriptionStatus.UNSUBSCRIBED
|
||||||
});
|
});
|
||||||
|
|
||||||
await tx('lists').where('id', listId).decrement('subscribers', 1);
|
await tx('lists').where('id', listId).decrement('subscribers', 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return existing;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function updateAddressAndGet(context, listId, subscriptionId, emailNew) {
|
||||||
|
return await knex.transaction(async tx => {
|
||||||
|
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSubscriptions');
|
||||||
|
|
||||||
|
const existing = await tx(getTableName(listId)).where('id', subscriptionId).first();
|
||||||
|
if (!existing) {
|
||||||
|
throw new interoperableErrors.NotFoundError();
|
||||||
|
}
|
||||||
|
|
||||||
|
await tx(getTableName(listId)).where('id', subscriptionId).update({
|
||||||
|
email: emailNew
|
||||||
|
});
|
||||||
|
|
||||||
|
existing.email = emailNew;
|
||||||
|
return existing;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
hashByList,
|
hashByList,
|
||||||
getById,
|
getById,
|
||||||
|
getByCid,
|
||||||
|
getByEmail,
|
||||||
list,
|
list,
|
||||||
listDTAjax,
|
listDTAjax,
|
||||||
serverValidate,
|
serverValidate,
|
||||||
create,
|
create,
|
||||||
updateWithConsistencyCheck,
|
updateWithConsistencyCheck,
|
||||||
remove,
|
remove,
|
||||||
unsubscribe
|
unsubscribeAndGet,
|
||||||
|
updateAddressAndGet
|
||||||
};
|
};
|
|
@ -1,9 +1,8 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
let users = require('../models/users');
|
|
||||||
let lists = require('../lib/models/lists');
|
let lists = require('../lib/models/lists');
|
||||||
let fields = require('../lib/models/fields');
|
let fields = require('../lib/models/fields');
|
||||||
let blacklist = require('../lib/models/blacklist');
|
let blacklist = require('../models/blacklist');
|
||||||
let subscriptions = require('../lib/models/subscriptions');
|
let subscriptions = require('../lib/models/subscriptions');
|
||||||
let confirmations = require('../lib/models/confirmations');
|
let confirmations = require('../lib/models/confirmations');
|
||||||
let tools = require('../lib/tools');
|
let tools = require('../lib/tools');
|
||||||
|
@ -12,35 +11,6 @@ const router = require('../lib/router-async').create();
|
||||||
let mailHelpers = require('../lib/subscription-mail-helpers');
|
let mailHelpers = require('../lib/subscription-mail-helpers');
|
||||||
const interoperableErrors = require('../shared/interoperable-errors');
|
const interoperableErrors = require('../shared/interoperable-errors');
|
||||||
|
|
||||||
router.allAsync('/*', async (req, res, next) => {
|
|
||||||
if (!req.query.access_token) {
|
|
||||||
res.status(403);
|
|
||||||
return res.json({
|
|
||||||
error: 'Missing access_token',
|
|
||||||
data: []
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await users.getByAccessToken(req.query.access_token);
|
|
||||||
next();
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof interoperableErrors.NotFoundError) {
|
|
||||||
res.status(403);
|
|
||||||
return res.json({
|
|
||||||
error: 'Invalid or expired access_token',
|
|
||||||
data: []
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
res.status(500);
|
|
||||||
return res.json({
|
|
||||||
error: err.message || err,
|
|
||||||
data: []
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/subscribe/:listId', (req, res) => {
|
router.post('/subscribe/:listId', (req, res) => {
|
||||||
let input = {};
|
let input = {};
|
||||||
Object.keys(req.body).forEach(key => {
|
Object.keys(req.body).forEach(key => {
|
||||||
|
@ -365,83 +335,53 @@ router.post('/field/:listId', (req, res) => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('/blacklist/add', (req, res) => {
|
router.postAsync('/blacklist/add', async (req, res) => {
|
||||||
let input = {};
|
let input = {};
|
||||||
Object.keys(req.body).forEach(key => {
|
Object.keys(req.body).forEach(key => {
|
||||||
input[(key || '').toString().trim().toUpperCase()] = (req.body[key] || '').toString().trim();
|
input[(key || '').toString().trim().toUpperCase()] = (req.body[key] || '').toString().trim();
|
||||||
});
|
});
|
||||||
if (!(input.EMAIL) || (input.EMAIL === '')) {
|
if (!(input.EMAIL) || (input.EMAIL === '')) {
|
||||||
res.status(500);
|
throw new Error('EMAIL argument is required');
|
||||||
return res.json({
|
|
||||||
error: 'EMAIL argument are required',
|
|
||||||
data: []
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
blacklist.add(input.EMAIL, (err) =>{
|
|
||||||
if (err) {
|
await blacklist.add(req.context, input.EMAIL);
|
||||||
res.status(500);
|
|
||||||
return res.json({
|
|
||||||
error: err.message || err,
|
|
||||||
data: []
|
|
||||||
});
|
|
||||||
}
|
|
||||||
res.status(200);
|
|
||||||
res.json({
|
res.json({
|
||||||
data: []
|
data: []
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('/blacklist/delete', (req, res) => {
|
router.postAsync('/blacklist/delete', async (req, res) => {
|
||||||
let input = {};
|
let input = {};
|
||||||
Object.keys(req.body).forEach(key => {
|
Object.keys(req.body).forEach(key => {
|
||||||
input[(key || '').toString().trim().toUpperCase()] = (req.body[key] || '').toString().trim();
|
input[(key || '').toString().trim().toUpperCase()] = (req.body[key] || '').toString().trim();
|
||||||
});
|
});
|
||||||
if (!(input.EMAIL) || (input.EMAIL === '')) {
|
if (!(input.EMAIL) || (input.EMAIL === '')) {
|
||||||
res.status(500);
|
throw new Error('EMAIL argument is required');
|
||||||
return res.json({
|
|
||||||
error: 'EMAIL argument are required',
|
|
||||||
data: []
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
blacklist.delete(input.EMAIL, (err) =>{
|
|
||||||
if (err) {
|
await blacklist.remove(req.oontext, input.EMAIL);
|
||||||
res.status(500);
|
|
||||||
return res.json({
|
|
||||||
error: err.message || err,
|
|
||||||
data: []
|
|
||||||
});
|
|
||||||
}
|
|
||||||
res.status(200);
|
|
||||||
res.json({
|
res.json({
|
||||||
data: []
|
data: []
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/blacklist/get', (req, res) => {
|
router.getAsync('/blacklist/get', async (req, res) => {
|
||||||
let start = parseInt(req.query.start || 0, 10);
|
let start = parseInt(req.query.start || 0, 10);
|
||||||
let limit = parseInt(req.query.limit || 10000, 10);
|
let limit = parseInt(req.query.limit || 10000, 10);
|
||||||
let search = req.query.search || '';
|
let search = req.query.search || '';
|
||||||
|
|
||||||
blacklist.get(start, limit, search, (err, data, total) => {
|
const { emails, total } = await blacklist.search(req.context, start, limit, search);
|
||||||
if (err) {
|
|
||||||
res.status(500);
|
|
||||||
return res.json({
|
return res.json({
|
||||||
error: err.message || err,
|
|
||||||
data: []
|
|
||||||
});
|
|
||||||
}
|
|
||||||
res.status(200);
|
|
||||||
res.json({
|
|
||||||
data: {
|
data: {
|
||||||
total: total,
|
total,
|
||||||
start: start,
|
start: start,
|
||||||
limit: limit,
|
limit: limit,
|
||||||
emails: data
|
emails
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
|
@ -11,7 +11,7 @@ router.postAsync('/lists-table', passport.loggedIn, async (req, res) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
router.getAsync('/lists/:listId', passport.loggedIn, async (req, res) => {
|
router.getAsync('/lists/:listId', passport.loggedIn, async (req, res) => {
|
||||||
const list = await lists.getById(req.context, req.params.listId);
|
const list = await lists.getByIdWithListFields(req.context, req.params.listId);
|
||||||
list.hash = lists.hash(list);
|
list.hash = lists.hash(list);
|
||||||
return res.json(list);
|
return res.json(list);
|
||||||
});
|
});
|
||||||
|
|
|
@ -39,7 +39,7 @@ router.postAsync('/subscriptions-validate/:listId', passport.loggedIn, async (re
|
||||||
});
|
});
|
||||||
|
|
||||||
router.postAsync('/subscriptions-unsubscribe/:listId/:subscriptionId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
|
router.postAsync('/subscriptions-unsubscribe/:listId/:subscriptionId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
|
||||||
await subscriptions.unsubscribe(req.context, req.params.listId, req.params.subscriptionId);
|
await subscriptions.unsubscribeAndGet(req.context, req.params.listId, req.params.subscriptionId);
|
||||||
return res.json();
|
return res.json();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
948
routes/subscription-legacy.js
Normal file
948
routes/subscription-legacy.js
Normal file
|
@ -0,0 +1,948 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
let log = require('npmlog');
|
||||||
|
let config = require('config');
|
||||||
|
let tools = require('../lib/tools');
|
||||||
|
let helpers = require('../lib/helpers');
|
||||||
|
let passport = require('../lib/passport');
|
||||||
|
let express = require('express');
|
||||||
|
let router = new express.Router();
|
||||||
|
let lists = require('../lib/models/lists');
|
||||||
|
let fields = require('../lib/models/fields');
|
||||||
|
let subscriptions = require('../lib/models/subscriptions');
|
||||||
|
let settings = require('../lib/models/settings');
|
||||||
|
let openpgp = require('openpgp');
|
||||||
|
let _ = require('../lib/translate')._;
|
||||||
|
let util = require('util');
|
||||||
|
let cors = require('cors');
|
||||||
|
let cache = require('memory-cache');
|
||||||
|
let geoip = require('geoip-ultralight');
|
||||||
|
let confirmations = require('../lib/models/confirmations');
|
||||||
|
let mailHelpers = require('../lib/subscription-mail-helpers');
|
||||||
|
|
||||||
|
let originWhitelist = config.cors && config.cors.origins || [];
|
||||||
|
|
||||||
|
let corsOptions = {
|
||||||
|
allowedHeaders: ['Content-Type', 'Origin', 'Accept', 'X-Requested-With'],
|
||||||
|
methods: ['GET', 'POST'],
|
||||||
|
optionsSuccessStatus: 200, // IE11 chokes on 204
|
||||||
|
origin: (origin, callback) => {
|
||||||
|
if (originWhitelist.includes(origin)) {
|
||||||
|
callback(null, true);
|
||||||
|
} else {
|
||||||
|
let err = new Error(_('Not allowed by CORS'));
|
||||||
|
err.status = 403;
|
||||||
|
callback(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let corsOrCsrfProtection = (req, res, next) => {
|
||||||
|
if (req.get('X-Requested-With') === 'XMLHttpRequest') {
|
||||||
|
cors(corsOptions)(req, res, next);
|
||||||
|
} else {
|
||||||
|
passport.csrfProtection(req, res, next);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function checkAndExecuteConfirmation(req, action, errorMsg, next, exec) {
|
||||||
|
confirmations.takeConfirmation(req.params.cid, (err, confirmation) => {
|
||||||
|
if (!err && (!confirmation || confirmation.action !== action)) {
|
||||||
|
err = new Error(_(errorMsg));
|
||||||
|
err.status = 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
lists.get(confirmation.listId, (err, list) => {
|
||||||
|
if (!err && !list) {
|
||||||
|
err = new Error(_('Selected list not found'));
|
||||||
|
err.status = 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
exec(confirmation, list);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
router.get('/confirm/subscribe/:cid', (req, res, next) => {
|
||||||
|
checkAndExecuteConfirmation(req, 'subscribe', 'Request invalid or already completed. If your subscription request is still pending, please subscribe again.', next, (confirmation, list) => {
|
||||||
|
const data = confirmation.data;
|
||||||
|
let optInCountry = geoip.lookupCountry(confirmation.ip) || null;
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
cid: req.params.cid,
|
||||||
|
email: data.email,
|
||||||
|
optInIp: confirmation.ip,
|
||||||
|
optInCountry,
|
||||||
|
status: subscriptions.Status.SUBSCRIBED
|
||||||
|
};
|
||||||
|
|
||||||
|
subscriptions.insert(list.id, meta, data.subscriptionData, (err, result) => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.entryId) {
|
||||||
|
return next(new Error(_('Could not save subscription')));
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriptions.getById(list.id, result.entryId, (err, subscription) => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
mailHelpers.sendSubscriptionConfirmed(list, data.email, subscription, err => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.redirect('/subscription/' + list.cid + '/subscribed-notice');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/confirm/change-address/:cid', (req, res, next) => {
|
||||||
|
checkAndExecuteConfirmation(req, 'change-address', 'Request invalid or already completed. If your address change request is still pending, please change the address again.', next, (confirmation, list) => {
|
||||||
|
const data = confirmation.data;
|
||||||
|
|
||||||
|
if (!data.subscriptionId) { // Something went terribly wrong and we don't have data that we have originally provided
|
||||||
|
return next(new Error(_('Subscriber info corrupted or missing')));
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriptions.updateAddress(list.id, data.subscriptionId, data.emailNew, err => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriptions.getById(list.id, data.subscriptionId, (err, subscription) => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
mailHelpers.sendSubscriptionConfirmed(list, data.emailNew, subscription, err => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
req.flash('info', _('Email address changed'));
|
||||||
|
res.redirect('/subscription/' + list.cid + '/manage/' + subscription.cid);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/confirm/unsubscribe/:cid', (req, res, next) => {
|
||||||
|
checkAndExecuteConfirmation(req, 'unsubscribe', 'Request invalid or already completed. If your unsubscription request is still pending, please unsubscribe again.', next, (confirmation, list) => {
|
||||||
|
const data = confirmation.data;
|
||||||
|
|
||||||
|
subscriptions.changeStatus(list.id, confirmation.data.subscriptionId, confirmation.data.campaignId, subscriptions.Status.UNSUBSCRIBED, (err, found) => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Shall we do anything with "found"?
|
||||||
|
|
||||||
|
subscriptions.getById(list.id, confirmation.data.subscriptionId, (err, subscription) => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
mailHelpers.sendUnsubscriptionConfirmed(list, subscription.email, subscription, err => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.redirect('/subscription/' + list.cid + '/unsubscribed-notice');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/:cid', passport.csrfProtection, (req, res, next) => {
|
||||||
|
lists.getByCid(req.params.cid, (err, list) => {
|
||||||
|
if (!err) {
|
||||||
|
if (!list) {
|
||||||
|
err = new Error(_('Selected list not found'));
|
||||||
|
err.status = 404;
|
||||||
|
} else if (!list.publicSubscribe) {
|
||||||
|
err = new Error(_('The list does not allow public subscriptions.'));
|
||||||
|
err.status = 403;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: process subscriber cid param for resubscription requests
|
||||||
|
|
||||||
|
let data = tools.convertKeys(req.query, {
|
||||||
|
skip: ['layout']
|
||||||
|
});
|
||||||
|
data.layout = 'subscription/layout';
|
||||||
|
data.title = list.name;
|
||||||
|
data.cid = list.cid;
|
||||||
|
data.csrfToken = req.csrfToken();
|
||||||
|
|
||||||
|
|
||||||
|
function nextStep() {
|
||||||
|
fields.list(list.id, (err, fieldList) => {
|
||||||
|
if (err && !fieldList) {
|
||||||
|
fieldList = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
data.customFields = fields.getRow(fieldList, data);
|
||||||
|
data.useEditor = true;
|
||||||
|
|
||||||
|
settings.list(['pgpPrivateKey', 'defaultAddress', 'defaultPostaddress'], (err, configItems) => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
data.hasPubkey = !!configItems.pgpPrivateKey;
|
||||||
|
data.defaultAddress = configItems.defaultAddress;
|
||||||
|
data.defaultPostaddress = configItems.defaultPostaddress;
|
||||||
|
|
||||||
|
data.template = {
|
||||||
|
template: 'subscription/web-subscribe.mjml.hbs',
|
||||||
|
layout: 'subscription/layout.mjml.hbs'
|
||||||
|
};
|
||||||
|
|
||||||
|
helpers.injectCustomFormData(req.query.fid || list.defaultForm, 'subscription/web-subscribe', data, (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
helpers.getMjmlTemplate(data.template, (err, htmlRenderer) => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
helpers.captureFlashMessages(req, res, (err, flash) => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
data.isWeb = true;
|
||||||
|
data.needsJsWarning = true;
|
||||||
|
data.flashMessages = flash;
|
||||||
|
res.send(htmlRenderer(data));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const ucid = req.query.cid;
|
||||||
|
if (ucid) {
|
||||||
|
subscriptions.get(list.id, ucid, (err, subscription) => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let key in subscription) {
|
||||||
|
if (!(key in data)) {
|
||||||
|
data[key] = subscription[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nextStep();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
nextStep();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.options('/:cid/widget', cors(corsOptions));
|
||||||
|
|
||||||
|
router.get('/:cid/widget', cors(corsOptions), (req, res, next) => {
|
||||||
|
let cached = cache.get(req.path);
|
||||||
|
if (cached) {
|
||||||
|
return res.status(200).json(cached);
|
||||||
|
}
|
||||||
|
|
||||||
|
let sendError = err => {
|
||||||
|
res.status(err.status || 500);
|
||||||
|
res.json({
|
||||||
|
error: err.message || err
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
lists.getByCid(req.params.cid, (err, list) => {
|
||||||
|
if (!err && !list) {
|
||||||
|
err = new Error(_('Selected list not found'));
|
||||||
|
err.status = 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
return sendError(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
fields.list(list.id, (err, fieldList) => {
|
||||||
|
if (err && !fieldList) {
|
||||||
|
fieldList = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.list(['serviceUrl', 'pgpPrivateKey'], (err, configItems) => {
|
||||||
|
if (err) {
|
||||||
|
return sendError(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = {
|
||||||
|
title: list.name,
|
||||||
|
cid: list.cid,
|
||||||
|
serviceUrl: configItems.serviceUrl,
|
||||||
|
hasPubkey: !!configItems.pgpPrivateKey,
|
||||||
|
customFields: fields.getRow(fieldList),
|
||||||
|
template: {},
|
||||||
|
layout: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
helpers.injectCustomFormData(req.query.fid || list.defaultForm, 'subscription/web-subscribe', data, (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
return sendError(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.render('subscription/widget-subscribe', data, (err, html) => {
|
||||||
|
if (err) {
|
||||||
|
return sendError(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = {
|
||||||
|
data: {
|
||||||
|
title: data.title,
|
||||||
|
cid: data.cid,
|
||||||
|
html
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
cache.put(req.path, response, 30000); // ms
|
||||||
|
res.status(200).json(response);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.options('/:cid/subscribe', cors(corsOptions));
|
||||||
|
|
||||||
|
router.post('/:cid/subscribe', passport.parseForm, corsOrCsrfProtection, (req, res, next) => {
|
||||||
|
let sendJsonError = (err, status) => {
|
||||||
|
res.status(status || err.status || 500);
|
||||||
|
res.json({
|
||||||
|
error: err.message || err
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let email = (req.body.email || '').toString().trim();
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
if (req.xhr) {
|
||||||
|
return sendJsonError(_('Email address not set'), 400);
|
||||||
|
}
|
||||||
|
req.flash('danger', _('Email address not set'));
|
||||||
|
return res.redirect('/subscription/' + encodeURIComponent(req.params.cid) + '?' + tools.queryParams(req.body));
|
||||||
|
}
|
||||||
|
|
||||||
|
tools.validateEmail(email, false, err => {
|
||||||
|
if (err) {
|
||||||
|
if (req.xhr) {
|
||||||
|
return sendJsonError(err.message, 400);
|
||||||
|
}
|
||||||
|
req.flash('danger', err.message);
|
||||||
|
return res.redirect('/subscription/' + encodeURIComponent(req.params.cid) + '?' + tools.queryParams(req.body));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the subscriber seems legit. This is a really simple check, the only requirement is that
|
||||||
|
// the subscriber has JavaScript turned on and thats it. If Mailtrain gets more targeted then this
|
||||||
|
// simple check should be replaced with an actual captcha
|
||||||
|
let subTime = Number(req.body.sub) || 0;
|
||||||
|
// allow clock skew 24h in the past and 24h to the future
|
||||||
|
let subTimeTest = !!(subTime > Date.now() - 24 * 3600 * 1000 && subTime < Date.now() + 24 * 3600 * 1000);
|
||||||
|
let addressTest = !req.body.address;
|
||||||
|
let testsPass = subTimeTest && addressTest;
|
||||||
|
|
||||||
|
lists.getByCid(req.params.cid, (err, list) => {
|
||||||
|
if (!err) {
|
||||||
|
if (!list) {
|
||||||
|
err = new Error(_('Selected list not found'));
|
||||||
|
err.status = 404;
|
||||||
|
} else if (!list.publicSubscribe) {
|
||||||
|
err = new Error(_('The list does not allow public subscriptions.'));
|
||||||
|
err.status = 403;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
return req.xhr ? sendJsonError(err) : next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
let subscriptionData = {};
|
||||||
|
Object.keys(req.body).forEach(key => {
|
||||||
|
if (key !== 'email' && key.charAt(0) !== '_') {
|
||||||
|
subscriptionData[key] = (req.body[key] || '').toString().trim();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
subscriptionData = tools.convertKeys(subscriptionData);
|
||||||
|
|
||||||
|
subscriptions.getByEmail(list.id, email, (err, subscription) => {
|
||||||
|
if (err) {
|
||||||
|
return req.xhr ? sendJsonError(err) : next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subscription && subscription.status === subscriptions.Status.SUBSCRIBED) {
|
||||||
|
mailHelpers.sendAlreadySubscribed(list, email, subscription, (err) => {
|
||||||
|
if (err) {
|
||||||
|
return req.xhr ? sendJsonError(err) : next(err);
|
||||||
|
}
|
||||||
|
res.redirect('/subscription/' + req.params.cid + '/confirm-subscription-notice');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const data = {
|
||||||
|
email,
|
||||||
|
subscriptionData
|
||||||
|
};
|
||||||
|
|
||||||
|
confirmations.addConfirmation(list.id, 'subscribe', req.ip, data, (err, confirmCid) => {
|
||||||
|
if (err) {
|
||||||
|
if (req.xhr) {
|
||||||
|
return sendJsonError(err);
|
||||||
|
}
|
||||||
|
req.flash('danger', err.message || err);
|
||||||
|
return res.redirect('/subscription/' + encodeURIComponent(req.params.cid) + '?' + tools.queryParams(req.body));
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendWebResponse() {
|
||||||
|
if (req.xhr) {
|
||||||
|
return res.status(200).json({
|
||||||
|
msg: _('Please Confirm Subscription')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
res.redirect('/subscription/' + req.params.cid + '/confirm-subscription-notice');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!testsPass) {
|
||||||
|
log.info('Subscription', 'Confirmation message for %s marked to be skipped (%s)', email, JSON.stringify(data));
|
||||||
|
sendWebResponse();
|
||||||
|
} else {
|
||||||
|
mailHelpers.sendConfirmSubscription(list, email, confirmCid, subscriptionData, (err) => {
|
||||||
|
if (err) {
|
||||||
|
return req.xhr ? sendJsonError(err) : sendWebResponse(err);
|
||||||
|
}
|
||||||
|
sendWebResponse();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/:lcid/manage/:ucid', passport.csrfProtection, (req, res, next) => {
|
||||||
|
lists.getByCid(req.params.lcid, (err, list) => {
|
||||||
|
if (!err && !list) {
|
||||||
|
err = new Error(_('Selected list not found'));
|
||||||
|
err.status = 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
fields.list(list.id, (err, fieldList) => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
subscriptions.get(list.id, req.params.ucid, (err, subscription) => {
|
||||||
|
if (!err && (!subscription || subscription.status !== subscriptions.Status.SUBSCRIBED)) {
|
||||||
|
err = new Error(_('Subscription not found in this list'));
|
||||||
|
err.status = 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
subscription.lcid = req.params.lcid;
|
||||||
|
subscription.title = list.name;
|
||||||
|
subscription.csrfToken = req.csrfToken();
|
||||||
|
subscription.layout = 'subscription/layout';
|
||||||
|
|
||||||
|
subscription.customFields = fields.getRow(fieldList, subscription);
|
||||||
|
|
||||||
|
subscription.useEditor = true;
|
||||||
|
|
||||||
|
settings.list(['pgpPrivateKey', 'defaultAddress', 'defaultPostaddress'], (err, configItems) => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
subscription.hasPubkey = !!configItems.pgpPrivateKey;
|
||||||
|
subscription.defaultAddress = configItems.defaultAddress;
|
||||||
|
subscription.defaultPostaddress = configItems.defaultPostaddress;
|
||||||
|
|
||||||
|
subscription.template = {
|
||||||
|
template: 'subscription/web-manage.mjml.hbs',
|
||||||
|
layout: 'subscription/layout.mjml.hbs'
|
||||||
|
};
|
||||||
|
|
||||||
|
helpers.injectCustomFormData(req.query.fid || list.defaultForm, 'subscription/web-manage', subscription, (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
helpers.getMjmlTemplate(data.template, (err, htmlRenderer) => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
helpers.captureFlashMessages(req, res, (err, flash) => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
data.isWeb = true;
|
||||||
|
data.needsJsWarning = true;
|
||||||
|
data.isManagePreferences = true;
|
||||||
|
data.flashMessages = flash;
|
||||||
|
res.send(htmlRenderer(data));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/:lcid/manage', passport.parseForm, passport.csrfProtection, (req, res, next) => {
|
||||||
|
lists.getByCid(req.params.lcid, (err, list) => {
|
||||||
|
if (!err && !list) {
|
||||||
|
err = new Error(_('Selected list not found'));
|
||||||
|
err.status = 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriptions.get(list.id, req.body.cid, (err, subscription) => {
|
||||||
|
if (!err && (!subscription || subscription.status !== subscriptions.Status.SUBSCRIBED)) {
|
||||||
|
err = new Error(_('Subscription not found in this list'));
|
||||||
|
err.status = 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriptions.update(list.id, subscription.cid, req.body, false, err => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
res.redirect('/subscription/' + req.params.lcid + '/updated-notice');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/:lcid/manage-address/:ucid', passport.csrfProtection, (req, res, next) => {
|
||||||
|
lists.getByCid(req.params.lcid, (err, list) => {
|
||||||
|
if (!err && !list) {
|
||||||
|
err = new Error(_('Selected list not found'));
|
||||||
|
err.status = 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.list(['defaultAddress', 'defaultPostaddress'], (err, configItems) => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriptions.get(list.id, req.params.ucid, (err, subscription) => {
|
||||||
|
if (!err && (!subscription || subscription.status !== subscriptions.Status.SUBSCRIBED)) {
|
||||||
|
err = new Error(_('Subscription not found in this list'));
|
||||||
|
err.status = 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
subscription.lcid = req.params.lcid;
|
||||||
|
subscription.title = list.name;
|
||||||
|
subscription.csrfToken = req.csrfToken();
|
||||||
|
subscription.defaultAddress = configItems.defaultAddress;
|
||||||
|
subscription.defaultPostaddress = configItems.defaultPostaddress;
|
||||||
|
|
||||||
|
subscription.template = {
|
||||||
|
template: 'subscription/web-manage-address.mjml.hbs',
|
||||||
|
layout: 'subscription/layout.mjml.hbs'
|
||||||
|
};
|
||||||
|
|
||||||
|
helpers.injectCustomFormData(req.query.fid || list.defaultForm, 'subscription/web-manage-address', subscription, (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
helpers.getMjmlTemplate(data.template, (err, htmlRenderer) => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
helpers.captureFlashMessages(req, res, (err, flash) => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
data.isWeb = true;
|
||||||
|
data.needsJsWarning = true;
|
||||||
|
data.flashMessages = flash;
|
||||||
|
res.send(htmlRenderer(data));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/:lcid/manage-address', passport.parseForm, passport.csrfProtection, (req, res, next) => {
|
||||||
|
lists.getByCid(req.params.lcid, (err, list) => {
|
||||||
|
if (!err && !list) {
|
||||||
|
err = new Error(_('Selected list not found'));
|
||||||
|
err.status = 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
let bodyData = tools.convertKeys(req.body); // This is here to convert "email-new" to "emailNew"
|
||||||
|
const emailOld = (bodyData.email || '').toString().trim();
|
||||||
|
const emailNew = (bodyData.emailNew || '').toString().trim();
|
||||||
|
|
||||||
|
if (emailOld === emailNew) {
|
||||||
|
req.flash('info', _('Nothing seems to be changed'));
|
||||||
|
res.redirect('/subscription/' + req.params.lcid + '/manage/' + req.body.cid);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
subscriptions.updateAddressCheck(list, req.body.cid, emailNew, req.ip, (err, subscription, newEmailAvailable) => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendWebResponse(err) {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
req.flash('info', _('An email with further instructions has been sent to the provided address'));
|
||||||
|
res.redirect('/subscription/' + req.params.lcid + '/manage/' + req.body.cid);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newEmailAvailable) {
|
||||||
|
const data = {
|
||||||
|
subscriptionId: subscription.id,
|
||||||
|
emailNew
|
||||||
|
};
|
||||||
|
|
||||||
|
confirmations.addConfirmation(list.id, 'change-address', req.ip, data, (err, confirmCid) => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
mailHelpers.sendConfirmAddressChange(list, emailNew, confirmCid, subscription, sendWebResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
mailHelpers.sendAlreadySubscribed(list, emailNew, subscription, sendWebResponse);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/:lcid/unsubscribe/:ucid', passport.csrfProtection, (req, res, next) => {
|
||||||
|
lists.getByCid(req.params.lcid, (err, list) => {
|
||||||
|
if (!err && !list) {
|
||||||
|
err = new Error(_('Selected list not found'));
|
||||||
|
err.status = 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.list(['defaultAddress', 'defaultPostaddress'], (err, configItems) => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriptions.get(list.id, req.params.ucid, (err, subscription) => {
|
||||||
|
if (!err && (!subscription || subscription.status !== subscriptions.Status.SUBSCRIBED)) {
|
||||||
|
err = new Error(_('Subscription not found in this list'));
|
||||||
|
err.status = 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const autoUnsubscribe = req.query.auto === 'yes';
|
||||||
|
|
||||||
|
if (autoUnsubscribe) {
|
||||||
|
handleUnsubscribe(list, subscription, autoUnsubscribe, req.query.c, req.ip, res, next);
|
||||||
|
|
||||||
|
} else if (req.query.formTest ||
|
||||||
|
list.unsubscriptionMode === lists.UnsubscriptionMode.ONE_STEP_WITH_FORM ||
|
||||||
|
list.unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP_WITH_FORM) {
|
||||||
|
|
||||||
|
subscription.lcid = req.params.lcid;
|
||||||
|
subscription.ucid = req.params.ucid;
|
||||||
|
subscription.title = list.name;
|
||||||
|
subscription.csrfToken = req.csrfToken();
|
||||||
|
subscription.campaign = req.query.c;
|
||||||
|
subscription.defaultAddress = configItems.defaultAddress;
|
||||||
|
subscription.defaultPostaddress = configItems.defaultPostaddress;
|
||||||
|
|
||||||
|
subscription.template = {
|
||||||
|
template: 'subscription/web-unsubscribe.mjml.hbs',
|
||||||
|
layout: 'subscription/layout.mjml.hbs'
|
||||||
|
};
|
||||||
|
|
||||||
|
helpers.injectCustomFormData(req.query.fid || list.defaultForm, 'subscription/web-unsubscribe', subscription, (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
helpers.getMjmlTemplate(data.template, (err, htmlRenderer) => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
helpers.captureFlashMessages(req, res, (err, flash) => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
data.isWeb = true;
|
||||||
|
data.flashMessages = flash;
|
||||||
|
res.send(htmlRenderer(data));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else { // UnsubscriptionMode.ONE_STEP || UnsubscriptionMode.TWO_STEP || UnsubscriptionMode.MANUAL
|
||||||
|
handleUnsubscribe(list, subscription, autoUnsubscribe, req.query.c, req.ip, res, next);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/:lcid/unsubscribe', passport.parseForm, passport.csrfProtection, (req, res, next) => {
|
||||||
|
lists.getByCid(req.params.lcid, (err, list) => {
|
||||||
|
if (!err && !list) {
|
||||||
|
err = new Error(_('Selected list not found'));
|
||||||
|
err.status = 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
const campaignId = (req.body.campaign || '').toString().trim() || false;
|
||||||
|
|
||||||
|
subscriptions.get(list.id, req.body.ucid, (err, subscription) => {
|
||||||
|
if (!err && (!subscription || subscription.status !== subscriptions.Status.SUBSCRIBED)) {
|
||||||
|
err = new Error(_('Subscription not found in this list'));
|
||||||
|
err.status = 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleUnsubscribe(list, subscription, false, campaignId, req.ip, res, next);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleUnsubscribe(list, subscription, autoUnsubscribe, campaignId, ip, res, next) {
|
||||||
|
if ((list.unsubscriptionMode === lists.UnsubscriptionMode.ONE_STEP || list.unsubscriptionMode === lists.UnsubscriptionMode.ONE_STEP_WITH_FORM) ||
|
||||||
|
(autoUnsubscribe && (list.unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP || list.unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP_WITH_FORM)) ) {
|
||||||
|
|
||||||
|
subscriptions.changeStatus(list.id, subscription.id, campaignId, subscriptions.Status.UNSUBSCRIBED, (err, found) => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Shall we do anything with "found"?
|
||||||
|
|
||||||
|
mailHelpers.sendUnsubscriptionConfirmed(list, subscription.email, subscription, err => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.redirect('/subscription/' + list.cid + '/unsubscribed-notice');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
} else if (list.unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP || list.unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP_WITH_FORM) {
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
subscriptionId: subscription.id,
|
||||||
|
campaignId
|
||||||
|
};
|
||||||
|
|
||||||
|
confirmations.addConfirmation(list.id, 'unsubscribe', ip, data, (err, confirmCid) => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
mailHelpers.sendConfirmUnsubscription(list, subscription.email, confirmCid, subscription, err => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.redirect('/subscription/' + list.cid + '/confirm-unsubscription-notice');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
} else { // UnsubscriptionMode.MANUAL
|
||||||
|
res.redirect('/subscription/' + list.cid + '/manual-unsubscribe-notice');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
router.get('/:cid/confirm-subscription-notice', (req, res, next) => {
|
||||||
|
webNotice('confirm-subscription', req, res, next);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/:cid/confirm-unsubscription-notice', (req, res, next) => {
|
||||||
|
webNotice('confirm-unsubscription', req, res, next);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/:cid/subscribed-notice', (req, res, next) => {
|
||||||
|
webNotice('subscribed', req, res, next);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/:cid/updated-notice', (req, res, next) => {
|
||||||
|
webNotice('updated', req, res, next);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/:cid/unsubscribed-notice', (req, res, next) => {
|
||||||
|
webNotice('unsubscribed', req, res, next);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/:cid/manual-unsubscribe-notice', (req, res, next) => {
|
||||||
|
webNotice('manual-unsubscribe', req, res, next);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/publickey', passport.parseForm, (req, res, next) => {
|
||||||
|
settings.list(['pgpPassphrase', 'pgpPrivateKey'], (err, configItems) => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
if (!configItems.pgpPrivateKey) {
|
||||||
|
err = new Error(_('Public key is not set'));
|
||||||
|
err.status = 404;
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
let privKey;
|
||||||
|
try {
|
||||||
|
privKey = openpgp.key.readArmored(configItems.pgpPrivateKey).keys[0];
|
||||||
|
if (configItems.pgpPassphrase && !privKey.decrypt(configItems.pgpPassphrase)) {
|
||||||
|
privKey = false;
|
||||||
|
}
|
||||||
|
} catch (E) {
|
||||||
|
// just ignore if failed
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!privKey) {
|
||||||
|
err = new Error(_('Public key is not set'));
|
||||||
|
err.status = 404;
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
let pubkey = privKey.toPublic().armor();
|
||||||
|
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Content-Type': 'application/octet-stream',
|
||||||
|
'Content-Disposition': 'attachment; filename=public.asc'
|
||||||
|
});
|
||||||
|
|
||||||
|
res.end(pubkey);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
function webNotice(type, req, res, next) {
|
||||||
|
lists.getByCid(req.params.cid, (err, list) => {
|
||||||
|
if (!err && !list) {
|
||||||
|
err = new Error(_('Selected list not found'));
|
||||||
|
err.status = 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.list(['defaultHomepage', 'serviceUrl', 'defaultAddress', 'defaultPostaddress', 'adminEmail'], (err, configItems) => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = {
|
||||||
|
title: list.name,
|
||||||
|
homepage: configItems.defaultHomepage || configItems.serviceUrl,
|
||||||
|
defaultAddress: configItems.defaultAddress,
|
||||||
|
defaultPostaddress: configItems.defaultPostaddress,
|
||||||
|
contactAddress: configItems.defaultAddress,
|
||||||
|
template: {
|
||||||
|
template: 'subscription/web-' + type + '-notice.mjml.hbs',
|
||||||
|
layout: 'subscription/layout.mjml.hbs'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
helpers.injectCustomFormData(req.query.fid || list.defaultForm, 'subscription/web-' + type + '-notice', data, (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
helpers.getMjmlTemplate(data.template, (err, htmlRenderer) => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
helpers.captureFlashMessages(req, res, (err, flash) => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
data.isWeb = true;
|
||||||
|
data.isConfirmNotice = true;
|
||||||
|
data.flashMessages = flash;
|
||||||
|
res.send(htmlRenderer(data));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = router;
|
|
@ -1,28 +1,43 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
let log = require('npmlog');
|
const log = require('npmlog');
|
||||||
let config = require('config');
|
const config = require('config');
|
||||||
let tools = require('../lib/tools');
|
const router = require('../lib/router-async').create();
|
||||||
let helpers = require('../lib/helpers');
|
const confirmations = require('../models/confirmations');
|
||||||
let passport = require('../lib/passport');
|
const subscriptions = require('../models/subscriptions');
|
||||||
let express = require('express');
|
const lists = require('../models/lists');
|
||||||
let router = new express.Router();
|
const fields = require('../models/fields');
|
||||||
let lists = require('../lib/models/lists');
|
const settings = require('../models/settings');
|
||||||
let fields = require('../lib/models/fields');
|
const _ = require('../lib/translate')._;
|
||||||
let subscriptions = require('../lib/models/subscriptions');
|
const contextHelpers = require('../lib/context-helpers');
|
||||||
let settings = require('../lib/models/settings');
|
const forms = require('../models/forms');
|
||||||
let openpgp = require('openpgp');
|
|
||||||
let _ = require('../lib/translate')._;
|
|
||||||
let util = require('util');
|
|
||||||
let cors = require('cors');
|
|
||||||
let cache = require('memory-cache');
|
|
||||||
let geoip = require('geoip-ultralight');
|
|
||||||
let confirmations = require('../lib/models/confirmations');
|
|
||||||
let mailHelpers = require('../lib/subscription-mail-helpers');
|
|
||||||
|
|
||||||
let originWhitelist = config.cors && config.cors.origins || [];
|
const openpgp = require('openpgp');
|
||||||
|
const util = require('util');
|
||||||
|
const cors = require('cors');
|
||||||
|
const cache = require('memory-cache');
|
||||||
|
const geoip = require('geoip-ultralight');
|
||||||
|
const passport = require('../lib/passport');
|
||||||
|
|
||||||
let corsOptions = {
|
const tools = require('../lib/tools-async');
|
||||||
|
const helpers = require('../lib/helpers');
|
||||||
|
const mailHelpers = require('../lib/subscription-mail-helpers');
|
||||||
|
|
||||||
|
const interoperableErrors = require('../shared/interoperable-errors');
|
||||||
|
|
||||||
|
const mjml = require('mjml');
|
||||||
|
const hbs = require('hbs');
|
||||||
|
|
||||||
|
const mjmlTemplates = new Map();
|
||||||
|
const objectHash = require('object-hash');
|
||||||
|
|
||||||
|
const bluebird = require('bluebird');
|
||||||
|
const fsReadFile = bluebird.promisify(require('fs').readFile);
|
||||||
|
|
||||||
|
|
||||||
|
const originWhitelist = config.cors && config.cors.origins || [];
|
||||||
|
|
||||||
|
const corsOptions = {
|
||||||
allowedHeaders: ['Content-Type', 'Origin', 'Accept', 'X-Requested-With'],
|
allowedHeaders: ['Content-Type', 'Origin', 'Accept', 'X-Requested-With'],
|
||||||
methods: ['GET', 'POST'],
|
methods: ['GET', 'POST'],
|
||||||
optionsSuccessStatus: 200, // IE11 chokes on 204
|
optionsSuccessStatus: 200, // IE11 chokes on 204
|
||||||
|
@ -30,14 +45,14 @@ let corsOptions = {
|
||||||
if (originWhitelist.includes(origin)) {
|
if (originWhitelist.includes(origin)) {
|
||||||
callback(null, true);
|
callback(null, true);
|
||||||
} else {
|
} else {
|
||||||
let err = new Error(_('Not allowed by CORS'));
|
const err = new Error(_('Not allowed by CORS'));
|
||||||
err.status = 403;
|
err.status = 403;
|
||||||
callback(err);
|
callback(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let corsOrCsrfProtection = (req, res, next) => {
|
const corsOrCsrfProtection = (req, res, next) => {
|
||||||
if (req.get('X-Requested-With') === 'XMLHttpRequest') {
|
if (req.get('X-Requested-With') === 'XMLHttpRequest') {
|
||||||
cors(corsOptions)(req, res, next);
|
cors(corsOptions)(req, res, next);
|
||||||
} else {
|
} else {
|
||||||
|
@ -45,170 +60,152 @@ let corsOrCsrfProtection = (req, res, next) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function checkAndExecuteConfirmation(req, action, errorMsg, next, exec) {
|
async function takeConfirmationAndValidate(req, action, errorFactory) {
|
||||||
confirmations.takeConfirmation(req.params.cid, (err, confirmation) => {
|
const confirmation = await confirmations.takeConfirmation(req.params.cid);
|
||||||
if (!err && (!confirmation || confirmation.action !== action)) {
|
|
||||||
err = new Error(_(errorMsg));
|
if (!confirmation || confirmation.action !== action) {
|
||||||
err.status = 404;
|
throw errorFactory();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (err) {
|
return confirmation;
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
lists.get(confirmation.listId, (err, list) => {
|
|
||||||
if (!err && !list) {
|
|
||||||
err = new Error(_('Selected list not found'));
|
|
||||||
err.status = 404;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
exec(confirmation, list);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
router.get('/confirm/subscribe/:cid', (req, res, next) => {
|
async function injectCustomFormData(customFormId, viewKey, data) {
|
||||||
checkAndExecuteConfirmation(req, 'subscribe', 'Request invalid or already completed. If your subscription request is still pending, please subscribe again.', next, (confirmation, list) => {
|
function sortAndFilterCustomFieldsBy(key) {
|
||||||
const data = confirmation.data;
|
data.customFields = data.customFields.filter(fld => fld[key] !== null);
|
||||||
let optInCountry = geoip.lookupCountry(confirmation.ip) || null;
|
data.customFields.sort((a, b) => a[key] - b[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewKey === 'web_subscribe') {
|
||||||
|
sortAndFilterCustomFieldsBy('order_subscribe');
|
||||||
|
} else if (viewKey === 'web_manage') {
|
||||||
|
sortAndFilterCustomFieldsBy('order_manage');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!customFormId) {
|
||||||
|
data.formInputStyle = '@import url(/subscription/form-input-style.css);';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const form = await forms.getById(contextHelpers.getAdminContext(), customFormId);
|
||||||
|
|
||||||
|
data.template.template = form[viewKey] || data.template.template;
|
||||||
|
data.template.layout = form.layout || data.template.layout;
|
||||||
|
data.formInputStyle = form.formInputStyle || '@import url(/subscription/form-input-style.css);';
|
||||||
|
|
||||||
|
const configItems = await settings.get(['ua_code']);
|
||||||
|
|
||||||
|
data.uaCode = configItems.uaCode;
|
||||||
|
data.customSubscriptionScripts = config.customsubscriptionscripts || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getMjmlTemplate(template) {
|
||||||
|
let key = (typeof template === 'object') ? objectHash(template) : template;
|
||||||
|
|
||||||
|
if (mjmlTemplates.has(key)) {
|
||||||
|
return mjmlTemplates.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
let source;
|
||||||
|
if (typeof template === 'object') {
|
||||||
|
source = await tools.mergeTemplateIntoLayout(template.template, template.layout);
|
||||||
|
} else {
|
||||||
|
source = await fsReadFile(path.join(__dirname, '..', 'views', template), 'utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
const compiled = mjml.mjml2html(source);
|
||||||
|
|
||||||
|
if (compiled.errors.length) {
|
||||||
|
throw new Error(compiled.errors[0].message || compiled.errors[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderer = hbs.handlebars.compile(compiled.html);
|
||||||
|
mjmlTemplates.set(key, renderer);
|
||||||
|
|
||||||
|
return renderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
function captureFlashMessages(req, res) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
res.render('subscription/capture-flash-messages', { layout: null }, (err, flash) => {
|
||||||
|
reject(err);
|
||||||
|
resolve(flash);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
router.getAsync('/confirm/subscribe/:cid', async (req, res) => {
|
||||||
|
const confirmation = await takeConfirmationAndValidate(req, 'subscribe', () => new interoperableErrors.InvalidConfirmationForSubscriptionError('Request invalid or already completed. If your subscription request is still pending, please subscribe again.'));
|
||||||
|
const subscription = confirmation.data;
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
cid: req.params.cid,
|
cid: req.params.cid,
|
||||||
email: data.email,
|
ip: confirmation.ip,
|
||||||
optInIp: confirmation.ip,
|
country: geoip.lookupCountry(confirmation.ip) || null
|
||||||
optInCountry,
|
|
||||||
status: subscriptions.Status.SUBSCRIBED
|
|
||||||
};
|
};
|
||||||
|
|
||||||
subscriptions.insert(list.id, meta, data.subscriptionData, (err, result) => {
|
subscription.status = SubscriptionStatus.SUBSCRIBED;
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!result.entryId) {
|
await subscriptions.create(contextHelpers.getAdminContext(), confirmation.list, subscription, meta);
|
||||||
return next(new Error(_('Could not save subscription')));
|
|
||||||
}
|
|
||||||
|
|
||||||
subscriptions.getById(list.id, result.entryId, (err, subscription) => {
|
const list = await lists.getById(contextHelpers.getAdminContext(), confirmation.list);
|
||||||
if (err) {
|
await mailHelpers.sendSubscriptionConfirmed(list, subscription.email, subscription);
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
mailHelpers.sendSubscriptionConfirmed(list, data.email, subscription, err => {
|
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
res.redirect('/subscription/' + list.cid + '/subscribed-notice');
|
res.redirect('/subscription/' + list.cid + '/subscribed-notice');
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/confirm/change-address/:cid', (req, res, next) => {
|
|
||||||
checkAndExecuteConfirmation(req, 'change-address', 'Request invalid or already completed. If your address change request is still pending, please change the address again.', next, (confirmation, list) => {
|
router.getAsync('/confirm/change-address/:cid', async (req, res) => {
|
||||||
|
const confirmation = await takeConfirmationAndValidate(req, 'change-address', () => new interoperableErrors.InvalidConfirmationForAddressChangeError('Request invalid or already completed. If your address change request is still pending, please change the address again.'));
|
||||||
const data = confirmation.data;
|
const data = confirmation.data;
|
||||||
|
|
||||||
if (!data.subscriptionId) { // Something went terribly wrong and we don't have data that we have originally provided
|
const subscription = await subscriptions.updateAddressAndGet(contextHelpers.getAdminContext(), list.id, data.subscriptionId, data.emailNew);
|
||||||
return next(new Error(_('Subscriber info corrupted or missing')));
|
|
||||||
}
|
|
||||||
|
|
||||||
subscriptions.updateAddress(list.id, data.subscriptionId, data.emailNew, err => {
|
await mailHelpers.sendSubscriptionConfirmed(list, data.emailNew, subscription);
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
subscriptions.getById(list.id, data.subscriptionId, (err, subscription) => {
|
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
mailHelpers.sendSubscriptionConfirmed(list, data.emailNew, subscription, err => {
|
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
req.flash('info', _('Email address changed'));
|
req.flash('info', _('Email address changed'));
|
||||||
res.redirect('/subscription/' + list.cid + '/manage/' + subscription.cid);
|
res.redirect('/subscription/' + list.cid + '/manage/' + subscription.cid);
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/confirm/unsubscribe/:cid', (req, res, next) => {
|
|
||||||
checkAndExecuteConfirmation(req, 'unsubscribe', 'Request invalid or already completed. If your unsubscription request is still pending, please unsubscribe again.', next, (confirmation, list) => {
|
router.getAsync('/confirm/unsubscribe/:cid', async (req, res) => {
|
||||||
|
const confirmation = await takeConfirmationAndValidate(req, 'unsubscribe', () => new interoperableErrors.InvalidConfirmationForUnsubscriptionError('Request invalid or already completed. If your unsubscription request is still pending, please unsubscribe again.'));
|
||||||
const data = confirmation.data;
|
const data = confirmation.data;
|
||||||
|
|
||||||
subscriptions.changeStatus(list.id, confirmation.data.subscriptionId, confirmation.data.campaignId, subscriptions.Status.UNSUBSCRIBED, (err, found) => {
|
const subscription = await subscriptions.unsubscribeAndGet(contextHelpers.getAdminContext(), list.id, data.subscriptionId);
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Shall we do anything with "found"?
|
await mailHelpers.sendUnsubscriptionConfirmed(list, subscription.email, subscription);
|
||||||
|
|
||||||
subscriptions.getById(list.id, confirmation.data.subscriptionId, (err, subscription) => {
|
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
mailHelpers.sendUnsubscriptionConfirmed(list, subscription.email, subscription, err => {
|
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
res.redirect('/subscription/' + list.cid + '/unsubscribed-notice');
|
res.redirect('/subscription/' + list.cid + '/unsubscribed-notice');
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/:cid', passport.csrfProtection, (req, res, next) => {
|
|
||||||
lists.getByCid(req.params.cid, (err, list) => {
|
router.getAsync('/:cid', passport.csrfProtection, async (req, res) => {
|
||||||
if (!err) {
|
const list = await lists.getByCid(req.params.cid);
|
||||||
if (!list) {
|
|
||||||
err = new Error(_('Selected list not found'));
|
if (!list.publicSubscribe) {
|
||||||
err.status = 404;
|
throw new interoperableErrors.SubscriptionNotAllowedError('The list does not allow public subscriptions.');
|
||||||
} else if (!list.publicSubscribe) {
|
|
||||||
err = new Error(_('The list does not allow public subscriptions.'));
|
|
||||||
err.status = 403;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (err) {
|
const ucid = req.query.cid;
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: process subscriber cid param for resubscription requests
|
const data = {};
|
||||||
|
|
||||||
let data = tools.convertKeys(req.query, {
|
|
||||||
skip: ['layout']
|
|
||||||
});
|
|
||||||
data.layout = 'subscription/layout';
|
data.layout = 'subscription/layout';
|
||||||
data.title = list.name;
|
data.title = list.name;
|
||||||
data.cid = list.cid;
|
data.cid = list.cid;
|
||||||
data.csrfToken = req.csrfToken();
|
data.csrfToken = req.csrfToken();
|
||||||
|
|
||||||
|
let subscription;
|
||||||
function nextStep() {
|
if (ucid) {
|
||||||
fields.list(list.id, (err, fieldList) => {
|
subscription = await subscriptions.getById(contextHelpers.getAdminContext(), list.id, ucid);
|
||||||
if (err && !fieldList) {
|
|
||||||
fieldList = [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data.customFields = fields.getRow(fieldList, data);
|
data.customFields = fields.getRow(contextHelpers.getAdminContext(), list.id, subscription);
|
||||||
data.useEditor = true;
|
data.useEditor = true;
|
||||||
|
|
||||||
settings.list(['pgpPrivateKey', 'defaultAddress', 'defaultPostaddress'], (err, configItems) => {
|
const configItems = await settings.get(['pgpPrivateKey', 'defaultAddress', 'defaultPostaddress']);
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
data.hasPubkey = !!configItems.pgpPrivateKey;
|
data.hasPubkey = !!configItems.pgpPrivateKey;
|
||||||
data.defaultAddress = configItems.defaultAddress;
|
data.defaultAddress = configItems.defaultAddress;
|
||||||
data.defaultPostaddress = configItems.defaultPostaddress;
|
data.defaultPostaddress = configItems.defaultPostaddress;
|
||||||
|
@ -218,110 +215,48 @@ router.get('/:cid', passport.csrfProtection, (req, res, next) => {
|
||||||
layout: 'subscription/layout.mjml.hbs'
|
layout: 'subscription/layout.mjml.hbs'
|
||||||
};
|
};
|
||||||
|
|
||||||
helpers.injectCustomFormData(req.query.fid || list.defaultForm, 'subscription/web-subscribe', data, (err, data) => {
|
await injectCustomFormData(req.query.fid || list.defaultForm, 'subscription/web-subscribe', data);
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
helpers.getMjmlTemplate(data.template, (err, htmlRenderer) => {
|
const htmlRenderer = await getMjmlTemplate(data.template);
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
helpers.captureFlashMessages(req, res, (err, flash) => {
|
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
data.isWeb = true;
|
data.isWeb = true;
|
||||||
data.needsJsWarning = true;
|
data.needsJsWarning = true;
|
||||||
data.flashMessages = flash;
|
data.flashMessages = await captureFlashMessages(res);
|
||||||
|
|
||||||
res.send(htmlRenderer(data));
|
res.send(htmlRenderer(data));
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const ucid = req.query.cid;
|
|
||||||
if (ucid) {
|
|
||||||
subscriptions.get(list.id, ucid, (err, subscription) => {
|
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let key in subscription) {
|
|
||||||
if (!(key in data)) {
|
|
||||||
data[key] = subscription[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nextStep();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
nextStep();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
router.options('/:cid/widget', cors(corsOptions));
|
router.options('/:cid/widget', cors(corsOptions));
|
||||||
|
|
||||||
router.get('/:cid/widget', cors(corsOptions), (req, res, next) => {
|
router.getAsync('/:cid/widget', cors(corsOptions), async (req, res) => {
|
||||||
let cached = cache.get(req.path);
|
req.needsAPIJSONResponse = true;
|
||||||
|
|
||||||
|
const cached = cache.get(req.path);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
return res.status(200).json(cached);
|
return res.status(200).json(cached);
|
||||||
}
|
}
|
||||||
|
|
||||||
let sendError = err => {
|
const list = await lists.getByCid(req.params.cid);
|
||||||
res.status(err.status || 500);
|
|
||||||
res.json({
|
|
||||||
error: err.message || err
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
lists.getByCid(req.params.cid, (err, list) => {
|
const configItems = settings.get(['serviceUrl', 'pgpPrivateKey']);
|
||||||
if (!err && !list) {
|
|
||||||
err = new Error(_('Selected list not found'));
|
|
||||||
err.status = 404;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (err) {
|
const data = {
|
||||||
return sendError(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
fields.list(list.id, (err, fieldList) => {
|
|
||||||
if (err && !fieldList) {
|
|
||||||
fieldList = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
settings.list(['serviceUrl', 'pgpPrivateKey'], (err, configItems) => {
|
|
||||||
if (err) {
|
|
||||||
return sendError(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
let data = {
|
|
||||||
title: list.name,
|
title: list.name,
|
||||||
cid: list.cid,
|
cid: list.cid,
|
||||||
serviceUrl: configItems.serviceUrl,
|
serviceUrl: configItems.serviceUrl,
|
||||||
hasPubkey: !!configItems.pgpPrivateKey,
|
hasPubkey: !!configItems.pgpPrivateKey,
|
||||||
customFields: fields.getRow(fieldList),
|
customFields: fields.getRow(contextHelpers.getAdminContext(), list.id),
|
||||||
template: {},
|
template: {},
|
||||||
layout: null,
|
layout: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
helpers.injectCustomFormData(req.query.fid || list.defaultForm, 'subscription/web-subscribe', data, (err, data) => {
|
await injectCustomFormData(req.query.fid || list.defaultForm, 'subscription/web-subscribe', data);
|
||||||
if (err) {
|
|
||||||
return sendError(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
res.render('subscription/widget-subscribe', data, (err, html) => {
|
const renderAsync = bluebird.promisify(res.render);
|
||||||
if (err) {
|
const html = await renderAsync('subscription/widget-subscribe', data);
|
||||||
return sendError(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
let response = {
|
const response = {
|
||||||
data: {
|
data: {
|
||||||
title: data.title,
|
title: data.title,
|
||||||
cid: data.cid,
|
cid: data.cid,
|
||||||
|
@ -331,39 +266,35 @@ router.get('/:cid/widget', cors(corsOptions), (req, res, next) => {
|
||||||
|
|
||||||
cache.put(req.path, response, 30000); // ms
|
cache.put(req.path, response, 30000); // ms
|
||||||
res.status(200).json(response);
|
res.status(200).json(response);
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
router.options('/:cid/subscribe', cors(corsOptions));
|
router.options('/:cid/subscribe', cors(corsOptions));
|
||||||
|
|
||||||
router.post('/:cid/subscribe', passport.parseForm, corsOrCsrfProtection, (req, res, next) => {
|
router.postAsync('/:cid/subscribe', passport.parseForm, corsOrCsrfProtection, async (req, res) => {
|
||||||
let sendJsonError = (err, status) => {
|
const email = (req.body.email || '').toString().trim();
|
||||||
res.status(status || err.status || 500);
|
|
||||||
res.json({
|
if (req.xhr) {
|
||||||
error: err.message || err
|
req.needsAPIJSONResponse = true;
|
||||||
});
|
}
|
||||||
};
|
|
||||||
|
|
||||||
let email = (req.body.email || '').toString().trim();
|
|
||||||
|
|
||||||
if (!email) {
|
if (!email) {
|
||||||
if (req.xhr) {
|
if (req.xhr) {
|
||||||
return sendJsonError(_('Email address not set'), 400);
|
throw new Error('Email address not set');
|
||||||
}
|
}
|
||||||
|
|
||||||
req.flash('danger', _('Email address not set'));
|
req.flash('danger', _('Email address not set'));
|
||||||
return res.redirect('/subscription/' + encodeURIComponent(req.params.cid) + '?' + tools.queryParams(req.body));
|
return res.redirect('/subscription/' + encodeURIComponent(req.params.cid) + '?' + tools.queryParams(req.body));
|
||||||
}
|
}
|
||||||
|
|
||||||
tools.validateEmail(email, false, err => {
|
const emailErr = await tools.validateEmail(email);
|
||||||
if (err) {
|
if (emailErr) {
|
||||||
if (req.xhr) {
|
if (req.xhr) {
|
||||||
return sendJsonError(err.message, 400);
|
throw new Error(emailErr.message);
|
||||||
}
|
}
|
||||||
req.flash('danger', err.message);
|
|
||||||
|
req.flash('danger', emailErr.message);
|
||||||
return res.redirect('/subscription/' + encodeURIComponent(req.params.cid) + '?' + tools.queryParams(req.body));
|
return res.redirect('/subscription/' + encodeURIComponent(req.params.cid) + '?' + tools.queryParams(req.body));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -376,20 +307,12 @@ router.post('/:cid/subscribe', passport.parseForm, corsOrCsrfProtection, (req, r
|
||||||
let addressTest = !req.body.address;
|
let addressTest = !req.body.address;
|
||||||
let testsPass = subTimeTest && addressTest;
|
let testsPass = subTimeTest && addressTest;
|
||||||
|
|
||||||
lists.getByCid(req.params.cid, (err, list) => {
|
const list = await lists.getByCid(req.params.cid);
|
||||||
if (!err) {
|
|
||||||
if (!list) {
|
if (!list.publicSubscribe) {
|
||||||
err = new Error(_('Selected list not found'));
|
throw new interoperableErrors.SubscriptionNotAllowedError('The list does not allow public subscriptions.');
|
||||||
err.status = 404;
|
|
||||||
} else if (!list.publicSubscribe) {
|
|
||||||
err = new Error(_('The list does not allow public subscriptions.'));
|
|
||||||
err.status = 403;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (err) {
|
|
||||||
return req.xhr ? sendJsonError(err) : next(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
let subscriptionData = {};
|
let subscriptionData = {};
|
||||||
Object.keys(req.body).forEach(key => {
|
Object.keys(req.body).forEach(key => {
|
||||||
|
@ -397,36 +320,27 @@ router.post('/:cid/subscribe', passport.parseForm, corsOrCsrfProtection, (req, r
|
||||||
subscriptionData[key] = (req.body[key] || '').toString().trim();
|
subscriptionData[key] = (req.body[key] || '').toString().trim();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
subscriptionData = tools.convertKeys(subscriptionData);
|
|
||||||
|
|
||||||
subscriptions.getByEmail(list.id, email, (err, subscription) => {
|
const subscription = subscriptions.getByEmail(list.id, email)
|
||||||
if (err) {
|
|
||||||
return req.xhr ? sendJsonError(err) : next(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (subscription && subscription.status === subscriptions.Status.SUBSCRIBED) {
|
if (subscription && subscription.status === subscriptions.Status.SUBSCRIBED) {
|
||||||
mailHelpers.sendAlreadySubscribed(list, email, subscription, (err) => {
|
await mailHelpers.sendAlreadySubscribed(list, email, subscription);
|
||||||
if (err) {
|
|
||||||
return req.xhr ? sendJsonError(err) : next(err);
|
|
||||||
}
|
|
||||||
res.redirect('/subscription/' + req.params.cid + '/confirm-subscription-notice');
|
res.redirect('/subscription/' + req.params.cid + '/confirm-subscription-notice');
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
const data = {
|
const data = {
|
||||||
email,
|
email,
|
||||||
subscriptionData
|
subscriptionData
|
||||||
};
|
};
|
||||||
|
|
||||||
confirmations.addConfirmation(list.id, 'subscribe', req.ip, data, (err, confirmCid) => {
|
const confirmCid = await confirmations.addConfirmation(list.id, 'subscribe', req.ip, data);
|
||||||
if (err) {
|
|
||||||
if (req.xhr) {
|
if (!testsPass) {
|
||||||
return sendJsonError(err);
|
log.info('Subscription', 'Confirmation message for %s marked to be skipped (%s)', email, JSON.stringify(data));
|
||||||
}
|
} else {
|
||||||
req.flash('danger', err.message || err);
|
await mailHelpers.sendConfirmSubscription(list, email, confirmCid, subscriptionData);
|
||||||
return res.redirect('/subscription/' + encodeURIComponent(req.params.cid) + '?' + tools.queryParams(req.body));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendWebResponse() {
|
|
||||||
if (req.xhr) {
|
if (req.xhr) {
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
msg: _('Please Confirm Subscription')
|
msg: _('Please Confirm Subscription')
|
||||||
|
@ -434,48 +348,14 @@ router.post('/:cid/subscribe', passport.parseForm, corsOrCsrfProtection, (req, r
|
||||||
}
|
}
|
||||||
res.redirect('/subscription/' + req.params.cid + '/confirm-subscription-notice');
|
res.redirect('/subscription/' + req.params.cid + '/confirm-subscription-notice');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!testsPass) {
|
|
||||||
log.info('Subscription', 'Confirmation message for %s marked to be skipped (%s)', email, JSON.stringify(data));
|
|
||||||
sendWebResponse();
|
|
||||||
} else {
|
|
||||||
mailHelpers.sendConfirmSubscription(list, email, confirmCid, subscriptionData, (err) => {
|
|
||||||
if (err) {
|
|
||||||
return req.xhr ? sendJsonError(err) : sendWebResponse(err);
|
|
||||||
}
|
|
||||||
sendWebResponse();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/:lcid/manage/:ucid', passport.csrfProtection, (req, res, next) => {
|
router.getAsync('/:lcid/manage/:ucid', passport.csrfProtection, async (req, res) => {
|
||||||
lists.getByCid(req.params.lcid, (err, list) => {
|
const list = await lists.getByCid(req.params.lcid);
|
||||||
if (!err && !list) {
|
const subscription = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, req.params.ucid);
|
||||||
err = new Error(_('Selected list not found'));
|
|
||||||
err.status = 404;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (err) {
|
if (!subscription || subscription.status !== subscriptions.Status.SUBSCRIBED) {
|
||||||
return next(err);
|
throw new Error(_('Subscription not found in this list'));
|
||||||
}
|
|
||||||
|
|
||||||
fields.list(list.id, (err, fieldList) => {
|
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
subscriptions.get(list.id, req.params.ucid, (err, subscription) => {
|
|
||||||
if (!err && (!subscription || subscription.status !== subscriptions.Status.SUBSCRIBED)) {
|
|
||||||
err = new Error(_('Subscription not found in this list'));
|
|
||||||
err.status = 404;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
subscription.lcid = req.params.lcid;
|
subscription.lcid = req.params.lcid;
|
||||||
|
@ -483,14 +363,11 @@ router.get('/:lcid/manage/:ucid', passport.csrfProtection, (req, res, next) => {
|
||||||
subscription.csrfToken = req.csrfToken();
|
subscription.csrfToken = req.csrfToken();
|
||||||
subscription.layout = 'subscription/layout';
|
subscription.layout = 'subscription/layout';
|
||||||
|
|
||||||
subscription.customFields = fields.getRow(fieldList, subscription);
|
subscription.customFields = await fields.getRow(contextHelpers.getAdminContext(), list.id, subscription);
|
||||||
|
|
||||||
subscription.useEditor = true;
|
subscription.useEditor = true;
|
||||||
|
|
||||||
settings.list(['pgpPrivateKey', 'defaultAddress', 'defaultPostaddress'], (err, configItems) => {
|
const configItems = await settings.get(['pgpPrivateKey', 'defaultAddress', 'defaultPostaddress']);
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
subscription.hasPubkey = !!configItems.pgpPrivateKey;
|
subscription.hasPubkey = !!configItems.pgpPrivateKey;
|
||||||
subscription.defaultAddress = configItems.defaultAddress;
|
subscription.defaultAddress = configItems.defaultAddress;
|
||||||
subscription.defaultPostaddress = configItems.defaultPostaddress;
|
subscription.defaultPostaddress = configItems.defaultPostaddress;
|
||||||
|
@ -500,64 +377,40 @@ router.get('/:lcid/manage/:ucid', passport.csrfProtection, (req, res, next) => {
|
||||||
layout: 'subscription/layout.mjml.hbs'
|
layout: 'subscription/layout.mjml.hbs'
|
||||||
};
|
};
|
||||||
|
|
||||||
helpers.injectCustomFormData(req.query.fid || list.defaultForm, 'subscription/web-manage', subscription, (err, data) => {
|
await injectCustomFormData(req.query.fid || list.defaultForm, 'subscription/web-manage', subscription);
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
helpers.getMjmlTemplate(data.template, (err, htmlRenderer) => {
|
const htmlRenderer = await getMjmlTemplate(data.template);
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
helpers.captureFlashMessages(req, res, (err, flash) => {
|
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
data.isWeb = true;
|
data.isWeb = true;
|
||||||
data.needsJsWarning = true;
|
data.needsJsWarning = true;
|
||||||
data.isManagePreferences = true;
|
data.isManagePreferences = true;
|
||||||
data.flashMessages = flash;
|
data.flashMessages = await captureFlashMessages(res);
|
||||||
|
|
||||||
res.send(htmlRenderer(data));
|
res.send(htmlRenderer(data));
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('/:lcid/manage', passport.parseForm, passport.csrfProtection, (req, res, next) => {
|
router.postAsync('/:lcid/manage', passport.parseForm, passport.csrfProtection, async (req, res) => {
|
||||||
lists.getByCid(req.params.lcid, (err, list) => {
|
const list = await lists.getByCid(req.params.lcid);
|
||||||
if (!err && !list) {
|
const subscription = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, req.body.cid);
|
||||||
err = new Error(_('Selected list not found'));
|
|
||||||
err.status = 404;
|
if (!subscription || subscription.status !== subscriptions.Status.SUBSCRIBED) {
|
||||||
|
throw new Error(_('Subscription not found in this list'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
subscriptions.get(list.id, req.body.cid, (err, subscription) => {
|
delete req.body.email; // email change is not allowed
|
||||||
if (!err && (!subscription || subscription.status !== subscriptions.Status.SUBSCRIBED)) {
|
delete req.body.status; // status change is not allowed
|
||||||
err = new Error(_('Subscription not found in this list'));
|
|
||||||
err.status = 404;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (err) {
|
// FIXME - az sem
|
||||||
return next(err);
|
// FIXME, allow update of only fields that have order_manage
|
||||||
}
|
|
||||||
|
|
||||||
|
await subscriptions.updateWithConsistencyCheck(contextHelpers.getAdminContext(), list.id, subscription)
|
||||||
subscriptions.update(list.id, subscription.cid, req.body, false, err => {
|
subscriptions.update(list.id, subscription.cid, req.body, false, err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return next(err);
|
return next(err);
|
||||||
}
|
}
|
||||||
res.redirect('/subscription/' + req.params.lcid + '/updated-notice');
|
res.redirect('/subscription/' + req.params.lcid + '/updated-notice');
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/:lcid/manage-address/:ucid', passport.csrfProtection, (req, res, next) => {
|
router.get('/:lcid/manage-address/:ucid', passport.csrfProtection, (req, res, next) => {
|
||||||
|
|
|
@ -87,6 +87,31 @@ class PermissionDeniedError extends InteroperableError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class InvalidConfirmationForSubscriptionError extends InteroperableError {
|
||||||
|
constructor(msg, data) {
|
||||||
|
super('InvalidConfirmationForSubscriptionError', msg, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class InvalidConfirmationForAddressChangeError extends InteroperableError {
|
||||||
|
constructor(msg, data) {
|
||||||
|
super('InvalidConfirmationForAddressChangeError', msg, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class InvalidConfirmationForUnsubscriptionError extends InteroperableError {
|
||||||
|
constructor(msg, data) {
|
||||||
|
super('InvalidConfirmationForUnsubscriptionError', msg, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SubscriptionNotAllowedError extends InteroperableError {
|
||||||
|
constructor(msg, data) {
|
||||||
|
super('SubscriptionNotAllowedError', msg, data);
|
||||||
|
this.status = 403;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const errorTypes = {
|
const errorTypes = {
|
||||||
InteroperableError,
|
InteroperableError,
|
||||||
NotLoggedInError,
|
NotLoggedInError,
|
||||||
|
@ -101,7 +126,11 @@ const errorTypes = {
|
||||||
InvalidTokenError,
|
InvalidTokenError,
|
||||||
DependencyNotFoundError,
|
DependencyNotFoundError,
|
||||||
NamespaceNotFoundError,
|
NamespaceNotFoundError,
|
||||||
PermissionDeniedError
|
PermissionDeniedError,
|
||||||
|
InvalidConfirmationForSubscriptionError,
|
||||||
|
InvalidConfirmationForAddressChangeError,
|
||||||
|
InvalidConfirmationForUnsubscriptionError,
|
||||||
|
SubscriptionNotAllowedError
|
||||||
};
|
};
|
||||||
|
|
||||||
function deserialize(errorObj) {
|
function deserialize(errorObj) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue