Subscription/unsubscription seems to work.
This commit is contained in:
parent
d8ee364a4b
commit
e9165838dc
22 changed files with 14939 additions and 196 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -3,7 +3,6 @@
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
package-lock.json
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
config/development.*
|
config/development.*
|
||||||
config/production.*
|
config/production.*
|
||||||
|
|
6159
client/package-lock.json
generated
Normal file
6159
client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -24,7 +24,6 @@
|
||||||
"immutable": "^3.8.1",
|
"immutable": "^3.8.1",
|
||||||
"moment": "^2.18.1",
|
"moment": "^2.18.1",
|
||||||
"moment-timezone": "^0.5.13",
|
"moment-timezone": "^0.5.13",
|
||||||
"owasp-password-strength-test": "github:bures/owasp-password-strength-test",
|
|
||||||
"prop-types": "^15.5.10",
|
"prop-types": "^15.5.10",
|
||||||
"querystringify": "^1.0.0",
|
"querystringify": "^1.0.0",
|
||||||
"react": "^15.6.1",
|
"react": "^15.6.1",
|
||||||
|
|
|
@ -56,7 +56,7 @@ export default class API extends Component {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Title>{t('Sign in')}</Title>
|
<Title>{t('API')}</Title>
|
||||||
|
|
||||||
|
|
||||||
<div className="panel panel-default">
|
<div className="panel panel-default">
|
||||||
|
|
|
@ -12,7 +12,7 @@ import {
|
||||||
Dropdown, Form,
|
Dropdown, Form,
|
||||||
withForm
|
withForm
|
||||||
} from '../../lib/form';
|
} from '../../lib/form';
|
||||||
import {Icon} from "../../lib/bootstrap-components";
|
import {Icon, Button} from "../../lib/bootstrap-components";
|
||||||
import axios from '../../lib/axios';
|
import axios from '../../lib/axios';
|
||||||
import {getFieldTypes, getSubscriptionStatusLabels} from './helpers';
|
import {getFieldTypes, getSubscriptionStatusLabels} from './helpers';
|
||||||
|
|
||||||
|
@ -154,6 +154,7 @@ export default class List extends Component {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
|
<a href={`/subscription/${this.props.list.cid}`} className="btn-default"><Button label={t('Subscription Form')} className="btn-default"/></a>
|
||||||
<NavButton linkTo={`/lists/${this.props.list.id}/subscriptions/create`} className="btn-primary" icon="plus" label={t('Add Subscriber')}/>
|
<NavButton linkTo={`/lists/${this.props.list.id}/subscriptions/create`} className="btn-primary" icon="plus" label={t('Add Subscriber')}/>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,10 @@ export function getFieldTypes(t) {
|
||||||
|
|
||||||
const stringFieldType = long => ({
|
const stringFieldType = long => ({
|
||||||
form: groupedField => long ? <TextArea key={getFieldKey(groupedField)} id={getFieldKey(groupedField)} label={groupedField.name}/> : <InputField key={getFieldKey(groupedField)} id={getFieldKey(groupedField)} label={groupedField.name}/>,
|
form: groupedField => long ? <TextArea key={getFieldKey(groupedField)} id={getFieldKey(groupedField)} label={groupedField.name}/> : <InputField key={getFieldKey(groupedField)} id={getFieldKey(groupedField)} label={groupedField.name}/>,
|
||||||
assignFormData: (groupedField, data) => {},
|
assignFormData: (groupedField, data) => {
|
||||||
|
const value = data[getFieldKey(groupedField)];
|
||||||
|
data[getFieldKey(groupedField)] = value || '';
|
||||||
|
},
|
||||||
initFormData: (groupedField, data) => {
|
initFormData: (groupedField, data) => {
|
||||||
data[getFieldKey(groupedField)] = '';
|
data[getFieldKey(groupedField)] = '';
|
||||||
},
|
},
|
||||||
|
@ -109,7 +112,10 @@ export function getFieldTypes(t) {
|
||||||
|
|
||||||
const jsonFieldType = {
|
const jsonFieldType = {
|
||||||
form: groupedField => <ACEEditor key={getFieldKey(groupedField)} id={getFieldKey(groupedField)} label={groupedField.name} mode="json" height="300px"/>,
|
form: groupedField => <ACEEditor key={getFieldKey(groupedField)} id={getFieldKey(groupedField)} label={groupedField.name} mode="json" height="300px"/>,
|
||||||
assignFormData: (groupedField, data) => {},
|
assignFormData: (groupedField, data) => {
|
||||||
|
const value = data[getFieldKey(groupedField)];
|
||||||
|
data[getFieldKey(groupedField)] = value || '';
|
||||||
|
},
|
||||||
initFormData: (groupedField, data) => {
|
initFormData: (groupedField, data) => {
|
||||||
data[getFieldKey(groupedField)] = '';
|
data[getFieldKey(groupedField)] = '';
|
||||||
},
|
},
|
||||||
|
|
|
@ -10,8 +10,7 @@ function getRequestContext(req) {
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAdminContext() {
|
const adminContext = {
|
||||||
const context = {
|
|
||||||
user: {
|
user: {
|
||||||
admin: true,
|
admin: true,
|
||||||
id: 0,
|
id: 0,
|
||||||
|
@ -19,9 +18,10 @@ function getAdminContext() {
|
||||||
name: '',
|
name: '',
|
||||||
email: ''
|
email: ''
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return context;
|
function getAdminContext() {
|
||||||
|
return adminContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
|
@ -166,13 +166,13 @@ async function ajaxListWithPermissionsTx(tx, context, fetchSpecs, params, queryF
|
||||||
|
|
||||||
async function ajaxList(params, queryFun, columns, options) {
|
async function ajaxList(params, queryFun, columns, options) {
|
||||||
return await knex.transaction(async tx => {
|
return await knex.transaction(async tx => {
|
||||||
return ajaxListTx(tx, params, queryFun, columns, options)
|
return await ajaxListTx(tx, params, queryFun, columns, options)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ajaxListWithPermissions(context, fetchSpecs, params, queryFun, columns, options) {
|
async function ajaxListWithPermissions(context, fetchSpecs, params, queryFun, columns, options) {
|
||||||
return await knex.transaction(async tx => {
|
return await knex.transaction(async tx => {
|
||||||
return ajaxListWithPermissionsTx(tx, context, fetchSpecs, params, queryFun, columns, options)
|
return await ajaxListWithPermissionsTx(tx, context, fetchSpecs, params, queryFun, columns, options)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ const knex = require('knex')({
|
||||||
migrations: {
|
migrations: {
|
||||||
directory: __dirname + '/../setup/knex/migrations'
|
directory: __dirname + '/../setup/knex/migrations'
|
||||||
}
|
}
|
||||||
, debug: true
|
//, debug: true
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = knex;
|
module.exports = knex;
|
||||||
|
|
|
@ -117,7 +117,7 @@ async function _sendMail(list, email, template, subject, relativeUrls, mailOpts,
|
||||||
|
|
||||||
const encryptionKeys = [];
|
const encryptionKeys = [];
|
||||||
for (const fld of flds) {
|
for (const fld of flds) {
|
||||||
if (fld.type === 'gpg' && field.value) {
|
if (fld.type === 'gpg' && fld.value) {
|
||||||
encryptionKeys.push(subscription[getFieldKey(fld)].value.trim());
|
encryptionKeys.push(subscription[getFieldKey(fld)].value.trim());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,10 +6,13 @@ const isemail = require('isemail');
|
||||||
|
|
||||||
const bluebird = require('bluebird');
|
const bluebird = require('bluebird');
|
||||||
const mergeTemplateIntoLayout = bluebird.promisify(require('./tools').mergeTemplateIntoLayout);
|
const mergeTemplateIntoLayout = bluebird.promisify(require('./tools').mergeTemplateIntoLayout);
|
||||||
|
const queryParams = require('./tools').queryParams;
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
validateEmail,
|
validateEmail,
|
||||||
mergeTemplateIntoLayout
|
validateEmailGetMessage,
|
||||||
|
mergeTemplateIntoLayout,
|
||||||
|
queryParams
|
||||||
};
|
};
|
||||||
|
|
||||||
async function validateEmail(address, checkBlocked) {
|
async function validateEmail(address, checkBlocked) {
|
||||||
|
@ -29,3 +32,21 @@ async function validateEmail(address, checkBlocked) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateEmailGetMessage(result, address) {
|
||||||
|
if (result !== 0) {
|
||||||
|
let message = util.format(_('Invalid email address "%s".'), address);
|
||||||
|
switch (result) {
|
||||||
|
case 5:
|
||||||
|
message += ' ' + _('MX record not found for domain');
|
||||||
|
break;
|
||||||
|
case 6:
|
||||||
|
message += ' ' + _('Address domain not found');
|
||||||
|
break;
|
||||||
|
case 12:
|
||||||
|
message += ' ' + _('Address domain name is required');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -87,7 +87,7 @@ async function _getById(tx, id) {
|
||||||
|
|
||||||
async function getById(context, id) {
|
async function getById(context, id) {
|
||||||
return await knex.transaction(async tx => {
|
return await knex.transaction(async tx => {
|
||||||
shares.enforceEntityPermissionTx(tx, context, 'customForm', id, 'view');
|
await shares.enforceEntityPermissionTx(tx, context, 'customForm', id, 'view');
|
||||||
const entity = await _getById(tx, id);
|
const entity = await _getById(tx, id);
|
||||||
entity.permissions = await shares.getPermissionsTx(tx, context, 'customForm', id);
|
entity.permissions = await shares.getPermissionsTx(tx, context, 'customForm', id);
|
||||||
return entity;
|
return entity;
|
||||||
|
@ -171,9 +171,9 @@ async function updateWithConsistencyCheck(context, entity) {
|
||||||
|
|
||||||
async function remove(context, id) {
|
async function remove(context, id) {
|
||||||
await knex.transaction(async tx => {
|
await knex.transaction(async tx => {
|
||||||
shares.enforceEntityPermissionTx(tx, context, 'customForm', id, 'delete');
|
await shares.enforceEntityPermissionTx(tx, context, 'customForm', id, 'delete');
|
||||||
|
|
||||||
lists.removeFormFromAllTx(tx, context, id);
|
await lists.removeFormFromAllTx(tx, context, id);
|
||||||
|
|
||||||
await tx('custom_forms_data').where('form', id).del();
|
await tx('custom_forms_data').where('form', id).del();
|
||||||
await tx('custom_forms').where('id', id).del();
|
await tx('custom_forms').where('id', id).del();
|
||||||
|
|
|
@ -33,21 +33,22 @@ async function listDTAjax(context, params) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _getByIdTx(tx, context, id) {
|
async function _getByIdTx(tx, context, id) {
|
||||||
shares.enforceEntityPermissionTx(tx, context, 'list', id, 'view');
|
await 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);
|
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getById(context, id) {
|
async function getById(context, id) {
|
||||||
return await knex.transaction(async tx => {
|
return await knex.transaction(async tx => {
|
||||||
return _getByIdTx(tx, context, id);
|
// note that permissions are not obtained here as this methods is used only with synthetic admin context
|
||||||
|
return await _getByIdTx(tx, context, id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getByIdWithListFields(context, id) {
|
async function getByIdWithListFields(context, id) {
|
||||||
return await knex.transaction(async tx => {
|
return await knex.transaction(async tx => {
|
||||||
const entity = _getByIdTx(tx, context, id);
|
const entity = await _getByIdTx(tx, context, id);
|
||||||
|
entity.permissions = await shares.getPermissionsTx(tx, context, 'list', id);
|
||||||
entity.listFields = await fields.listByOrderListTx(tx, id);
|
entity.listFields = await fields.listByOrderListTx(tx, id);
|
||||||
return entity;
|
return entity;
|
||||||
});
|
});
|
||||||
|
@ -60,8 +61,7 @@ async function getByCid(context, cid) {
|
||||||
shares.throwPermissionDenied();
|
shares.throwPermissionDenied();
|
||||||
}
|
}
|
||||||
|
|
||||||
shares.enforceEntityPermissionTx(tx, context, 'list', entity.id, 'view');
|
await shares.enforceEntityPermissionTx(tx, context, 'list', entity.id, 'view');
|
||||||
entity.permissions = await shares.getPermissionsTx(tx, context, 'list', entity.id);
|
|
||||||
return entity;
|
return entity;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,11 +31,16 @@ function getOptionsMap(groupedField) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldTypes.text = fieldTypes.website = fieldTypes.longtext = fieldTypes.gpg = fieldTypes.number = {
|
fieldTypes.text = fieldTypes.website = fieldTypes.longtext = fieldTypes.gpg = {
|
||||||
afterJSON: (groupedField, entity) => {},
|
afterJSON: (groupedField, entity) => {},
|
||||||
listRender: (groupedField, value) => value
|
listRender: (groupedField, value) => value
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fieldTypes.number = {
|
||||||
|
afterJSON: (groupedField, entity) => {},
|
||||||
|
listRender: (groupedField, value) => Number(value)
|
||||||
|
};
|
||||||
|
|
||||||
fieldTypes.json = {
|
fieldTypes.json = {
|
||||||
afterJSON: (groupedField, entity) => {},
|
afterJSON: (groupedField, entity) => {},
|
||||||
listRender: (groupedField, value) => value
|
listRender: (groupedField, value) => value
|
||||||
|
@ -59,14 +64,20 @@ fieldTypes['radio-enum'] = fieldTypes['dropdown-enum'] = fieldTypes['radio-group
|
||||||
|
|
||||||
fieldTypes.date = {
|
fieldTypes.date = {
|
||||||
afterJSON: (groupedField, entity) => {
|
afterJSON: (groupedField, entity) => {
|
||||||
entity[getFieldKey(groupedField)] = moment(entity[getFieldKey(groupedField)]).toDate();
|
const key = getFieldKey(groupedField);
|
||||||
|
if (key in entity) {
|
||||||
|
entity[key] = entity[key] ? moment(entity[key]).toDate() : null;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
listRender: (groupedField, value) => formatDate(groupedField.settings.dateFormat, value)
|
listRender: (groupedField, value) => formatDate(groupedField.settings.dateFormat, value)
|
||||||
};
|
};
|
||||||
|
|
||||||
fieldTypes.birthday = {
|
fieldTypes.birthday = {
|
||||||
afterJSON: (groupedField, entity) => {
|
afterJSON: (groupedField, entity) => {
|
||||||
entity[getFieldKey(groupedField)] = moment(entity[getFieldKey(groupedField)]).toDate();
|
const key = getFieldKey(groupedField);
|
||||||
|
if (key in entity) {
|
||||||
|
entity[key] = entity[key] ? moment(entity[key]).toDate() : null;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
listRender: (groupedField, value) => formatBirthday(groupedField.settings.dateFormat, value)
|
listRender: (groupedField, value) => formatBirthday(groupedField.settings.dateFormat, value)
|
||||||
};
|
};
|
||||||
|
@ -153,7 +164,7 @@ function ungroupSubscription(groupedFieldsMap, entity) {
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
const values = entity[fldKey];
|
const values = entity[fldKey] || []; // The default (empty array) is here because create may be called with an entity that has some fields not filled in
|
||||||
for (const optionKey in fld.groupedOptions) {
|
for (const optionKey in fld.groupedOptions) {
|
||||||
const option = fld.groupedOptions[optionKey];
|
const option = fld.groupedOptions[optionKey];
|
||||||
entity[option.column] = values.includes(option.column);
|
entity[option.column] = values.includes(option.column);
|
||||||
|
@ -210,11 +221,6 @@ async function _getStatusBy(context, listId, key, value) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getStatusByCid(context, listId, cid) {
|
|
||||||
return await _getStatusBy(context, listId, 'cid', cid);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async function _getBy(context, listId, key, value, grouped) {
|
async function _getBy(context, listId, key, value, grouped) {
|
||||||
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');
|
||||||
|
@ -387,8 +393,9 @@ async function serverValidate(context, listId, data) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _validateAndPreprocess(tx, listId, groupedFieldsMap, entity, isCreate) {
|
async function _validateAndPreprocess(tx, listId, groupedFieldsMap, entity, meta, isCreate) {
|
||||||
enforce(entity.email, 'Email must be set');
|
enforce(entity.email, 'Email must be set');
|
||||||
|
enforce(entity.status > 0 && entity.status < SubscriptionStatus.MAX, 'Subscription status is invalid');
|
||||||
|
|
||||||
const existingWithKeyQuery = tx(getSubscriptionTableName(listId)).where('email', entity.email);
|
const existingWithKeyQuery = tx(getSubscriptionTableName(listId)).where('email', entity.email);
|
||||||
|
|
||||||
|
@ -397,8 +404,13 @@ async function _validateAndPreprocess(tx, listId, groupedFieldsMap, entity, isCr
|
||||||
}
|
}
|
||||||
const existingWithKey = await existingWithKeyQuery.first();
|
const existingWithKey = await existingWithKeyQuery.first();
|
||||||
if (existingWithKey) {
|
if (existingWithKey) {
|
||||||
|
if (meta && meta.replaceOfUnsubscribedAllowed && existingWithKey.status === SubscriptionStatus.UNSUBSCRIBED) {
|
||||||
|
meta.updateNeeded = true;
|
||||||
|
meta.existing = existingWithKey;
|
||||||
|
} else {
|
||||||
throw new interoperableErrors.DuplicitEmailError();
|
throw new interoperableErrors.DuplicitEmailError();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enforce(entity.status >= 0 && entity.status < SubscriptionStatus.MAX, 'Invalid status');
|
enforce(entity.status >= 0 && entity.status < SubscriptionStatus.MAX, 'Invalid status');
|
||||||
|
|
||||||
|
@ -409,33 +421,68 @@ async function _validateAndPreprocess(tx, listId, groupedFieldsMap, entity, isCr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function create(context, listId, entity, meta = {}) {
|
async function _update(tx, listId, existing, filteredEntity) {
|
||||||
|
if (existing.status !== filteredEntity.status) {
|
||||||
|
filteredEntity.status_change = new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
await tx(getSubscriptionTableName(listId)).where('id', existing.id).update(filteredEntity);
|
||||||
|
|
||||||
|
|
||||||
|
let countIncrement = 0;
|
||||||
|
if (existing.status === SubscriptionStatus.SUBSCRIBED && filteredEntity.status !== SubscriptionStatus.SUBSCRIBED) {
|
||||||
|
countIncrement = -1;
|
||||||
|
} else if (existing.status !== SubscriptionStatus.SUBSCRIBED && filteredEntity.status === SubscriptionStatus.SUBSCRIBED) {
|
||||||
|
countIncrement = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (countIncrement) {
|
||||||
|
await tx('lists').where('id', listId).increment('subscribers', countIncrement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _create(tx, listId, filteredEntity) {
|
||||||
|
const ids = await tx(getSubscriptionTableName(listId)).insert(filteredEntity);
|
||||||
|
const id = ids[0];
|
||||||
|
|
||||||
|
if (filteredEntity.status === SubscriptionStatus.SUBSCRIBED) {
|
||||||
|
await tx('lists').where('id', listId).increment('subscribers', 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function create(context, listId, entity, meta /* meta is provided when called from /confirm/subscribe/:cid */) {
|
||||||
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');
|
||||||
|
|
||||||
const groupedFieldsMap = await getGroupedFieldsMap(tx, listId);
|
const groupedFieldsMap = await getGroupedFieldsMap(tx, listId);
|
||||||
const allowedKeys = getAllowedKeys(groupedFieldsMap);
|
const allowedKeys = getAllowedKeys(groupedFieldsMap);
|
||||||
|
|
||||||
await _validateAndPreprocess(tx, listId, groupedFieldsMap, entity, true);
|
await _validateAndPreprocess(tx, listId, groupedFieldsMap, entity, meta, true);
|
||||||
|
|
||||||
const filteredEntity = filterObject(entity, allowedKeys);
|
const filteredEntity = filterObject(entity, allowedKeys);
|
||||||
filteredEntity.cid = shortid.generate();
|
|
||||||
filteredEntity.status_change = new Date();
|
filteredEntity.status_change = new Date();
|
||||||
|
|
||||||
ungroupSubscription(groupedFieldsMap, filteredEntity);
|
ungroupSubscription(groupedFieldsMap, filteredEntity);
|
||||||
|
|
||||||
filteredEntity.opt_in_ip = meta.ip;
|
filteredEntity.opt_in_ip = meta && meta.ip;
|
||||||
filteredEntity.opt_in_country = meta.country;
|
filteredEntity.opt_in_country = meta && meta.country;
|
||||||
filteredEntity.imported = meta.imported || false;
|
filteredEntity.imported = meta && !!meta.imported;
|
||||||
|
|
||||||
const ids = await tx(getSubscriptionTableName(listId)).insert(filteredEntity);
|
if (meta && meta.updateNeeded) {
|
||||||
const id = ids[0];
|
await _update(tx, listId, meta.existing, filteredEntity);
|
||||||
|
meta.cid = meta.existing.cid; // The cid is needed by /confirm/subscribe/:cid
|
||||||
|
return meta.existing.id;
|
||||||
|
} else {
|
||||||
|
filteredEntity.cid = shortid.generate();
|
||||||
|
|
||||||
if (entity.status === SubscriptionStatus.SUBSCRIBED) {
|
if (meta) {
|
||||||
await tx('lists').where('id', listId).increment('subscribers', 1);
|
meta.cid = filteredEntity.cid; // The cid is needed by /confirm/subscribe/:cid
|
||||||
}
|
}
|
||||||
|
|
||||||
return id;
|
return await _create(tx, listId, filteredEntity);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -458,29 +505,13 @@ async function updateWithConsistencyCheck(context, listId, entity) {
|
||||||
throw new interoperableErrors.ChangedError();
|
throw new interoperableErrors.ChangedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
await _validateAndPreprocess(tx, listId, groupedFieldsMap, entity, false);
|
await _validateAndPreprocess(tx, listId, groupedFieldsMap, entity, null, false);
|
||||||
|
|
||||||
const filteredEntity = filterObject(entity, allowedKeys);
|
const filteredEntity = filterObject(entity, allowedKeys);
|
||||||
|
|
||||||
ungroupSubscription(groupedFieldsMap, filteredEntity);
|
ungroupSubscription(groupedFieldsMap, filteredEntity);
|
||||||
|
|
||||||
if (existing.status !== entity.status) {
|
await _update(tx, listId, existing, filteredEntity);
|
||||||
filteredEntity.status_change = new Date();
|
|
||||||
}
|
|
||||||
|
|
||||||
await tx(getSubscriptionTableName(listId)).where('id', entity.id).update(filteredEntity);
|
|
||||||
|
|
||||||
|
|
||||||
let countIncrement = 0;
|
|
||||||
if (existing.status === SubscriptionStatus.SUBSCRIBED && entity.status !== SubscriptionStatus.SUBSCRIBED) {
|
|
||||||
countIncrement = -1;
|
|
||||||
} else if (existing.status !== SubscriptionStatus.SUBSCRIBED && entity.status === SubscriptionStatus.SUBSCRIBED) {
|
|
||||||
countIncrement = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (countIncrement) {
|
|
||||||
await tx('lists').where('id', listId).increment('subscribers', countIncrement);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -505,18 +536,16 @@ async function remove(context, listId, id) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function unsubscribeByCidAndGet(context, listId, subscriptionCid, campaignCid) {
|
async function _unsubscribeAndGetTx(tx, context, listId, existingSubscription, campaignCid) {
|
||||||
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(getSubscriptionTableName(listId)).where('cid', subscriptionCid).first();
|
if (!(existingSubscription && existingSubscription.status === SubscriptionStatus.SUBSCRIBED)) {
|
||||||
if (!(existing && existing.status === SubscriptionStatus.SUBSCRIBED)) {
|
|
||||||
throw new interoperableErrors.NotFoundError();
|
throw new interoperableErrors.NotFoundError();
|
||||||
}
|
}
|
||||||
|
|
||||||
existing.status = SubscriptionStatus.UNSUBSCRIBED;
|
existingSubscription.status = SubscriptionStatus.UNSUBSCRIBED;
|
||||||
|
|
||||||
await tx(getSubscriptionTableName(listId)).where('cid', subscriptionCid).update({
|
await tx(getSubscriptionTableName(listId)).where('id', existingSubscription.id).update({
|
||||||
status: SubscriptionStatus.UNSUBSCRIBED
|
status: SubscriptionStatus.UNSUBSCRIBED
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -524,7 +553,7 @@ async function unsubscribeByCidAndGet(context, listId, subscriptionCid, campaign
|
||||||
|
|
||||||
if (campaignCid) {
|
if (campaignCid) {
|
||||||
const campaign = await tx('campaigns').where('cid', campaignCid);
|
const campaign = await tx('campaigns').where('cid', campaignCid);
|
||||||
const subscriptionInCampaign = await tx(getCampaignTableName(campaign.id)).where({subscription: existing.id, list: listId});
|
const subscriptionInCampaign = await tx(getCampaignTableName(campaign.id)).where({subscription: existingSubscription.id, list: listId});
|
||||||
|
|
||||||
if (!subscriptionInCampaign) {
|
if (!subscriptionInCampaign) {
|
||||||
throw new Error('Invalid campaign.')
|
throw new Error('Invalid campaign.')
|
||||||
|
@ -532,13 +561,27 @@ async function unsubscribeByCidAndGet(context, listId, subscriptionCid, campaign
|
||||||
|
|
||||||
if (subscriptionInCampaign.status === SubscriptionStatus.SUBSCRIBED) {
|
if (subscriptionInCampaign.status === SubscriptionStatus.SUBSCRIBED) {
|
||||||
await tx('campaigns').where('id', campaign.id).increment('unsubscribed', 1);
|
await tx('campaigns').where('id', campaign.id).increment('unsubscribed', 1);
|
||||||
await tx(getCampaignTableName(campaign.id)).where({subscription: existing.id, list: listId}).update({
|
await tx(getCampaignTableName(campaign.id)).where({subscription: existingSubscription.id, list: listId}).update({
|
||||||
status: SubscriptionStatus.UNSUBSCRIBED
|
status: SubscriptionStatus.UNSUBSCRIBED
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return existing;
|
return existingSubscription;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function unsubscribeByIdAndGet(context, listId, subscriptionId) {
|
||||||
|
return await knex.transaction(async tx => {
|
||||||
|
const existing = await tx(getSubscriptionTableName(listId)).where('id', subscriptionId).first();
|
||||||
|
return _unsubscribeAndGetTx(tx, context, listId, existing);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function unsubscribeByCidAndGet(context, listId, subscriptionCid, campaignCid) {
|
||||||
|
return await knex.transaction(async tx => {
|
||||||
|
const existing = await tx(getSubscriptionTableName(listId)).where('cid', subscriptionCid).first();
|
||||||
|
return _unsubscribeAndGetTx(tx, context, listId, existing, campaignCid);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -564,8 +607,8 @@ async function updateManagedUngrouped(context, listId, entity) {
|
||||||
await knex.transaction(async tx => {
|
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(getSubscriptionTableName(listId)).where('id', entity.id).first();
|
const existing = await tx(getSubscriptionTableName(listId)).where('cid', entity.cid).first();
|
||||||
if (!existing) {
|
if (!existing || existing.status !== SubscriptionStatus.SUBSCRIBED) {
|
||||||
throw new interoperableErrors.NotFoundError();
|
throw new interoperableErrors.NotFoundError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -583,7 +626,7 @@ async function updateManagedUngrouped(context, listId, entity) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await tx(getSubscriptionTableName(listId)).where('id', entity.id).update(update);
|
await tx(getSubscriptionTableName(listId)).where('cid', entity.cid).update(update);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -592,7 +635,6 @@ module.exports = {
|
||||||
getById,
|
getById,
|
||||||
getByCid,
|
getByCid,
|
||||||
getByEmail,
|
getByEmail,
|
||||||
getStatusByCid,
|
|
||||||
list,
|
list,
|
||||||
listDTAjax,
|
listDTAjax,
|
||||||
serverValidate,
|
serverValidate,
|
||||||
|
@ -600,6 +642,7 @@ module.exports = {
|
||||||
updateWithConsistencyCheck,
|
updateWithConsistencyCheck,
|
||||||
remove,
|
remove,
|
||||||
unsubscribeByCidAndGet,
|
unsubscribeByCidAndGet,
|
||||||
|
unsubscribeByIdAndGet,
|
||||||
updateAddressAndGet,
|
updateAddressAndGet,
|
||||||
updateManagedUngrouped
|
updateManagedUngrouped
|
||||||
};
|
};
|
8447
package-lock.json
generated
Normal file
8447
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "mailtrain",
|
"name": "mailtrain",
|
||||||
"private": true,
|
|
||||||
"version": "1.23.2",
|
"version": "1.23.2",
|
||||||
"description": "Self hosted email newsletter app",
|
"description": "Self hosted email newsletter app",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
|
@ -29,7 +28,6 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-eslint": "^8.1.2",
|
"babel-eslint": "^8.1.2",
|
||||||
"bluebird": "^3.5.0",
|
|
||||||
"chai": "^4.1.2",
|
"chai": "^4.1.2",
|
||||||
"eslint-config-nodemailer": "^1.2.0",
|
"eslint-config-nodemailer": "^1.2.0",
|
||||||
"grunt": "^1.0.1",
|
"grunt": "^1.0.1",
|
||||||
|
@ -91,6 +89,7 @@
|
||||||
"memory-cache": "^0.2.0",
|
"memory-cache": "^0.2.0",
|
||||||
"mjml": "3.3.5",
|
"mjml": "3.3.5",
|
||||||
"mkdirp": "^0.5.1",
|
"mkdirp": "^0.5.1",
|
||||||
|
"moment": "^2.18.1",
|
||||||
"moment-timezone": "^0.5.13",
|
"moment-timezone": "^0.5.13",
|
||||||
"morgan": "^1.8.2",
|
"morgan": "^1.8.2",
|
||||||
"multer": "^1.3.0",
|
"multer": "^1.3.0",
|
||||||
|
@ -105,7 +104,6 @@
|
||||||
"npmlog": "^4.1.2",
|
"npmlog": "^4.1.2",
|
||||||
"object-hash": "^1.1.8",
|
"object-hash": "^1.1.8",
|
||||||
"openpgp": "^2.6.1",
|
"openpgp": "^2.6.1",
|
||||||
"owasp-password-strength-test": "github:bures/owasp-password-strength-test",
|
|
||||||
"passport": "^0.4.0",
|
"passport": "^0.4.0",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
"premailer-api": "^1.0.4",
|
"premailer-api": "^1.0.4",
|
||||||
|
|
|
@ -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.unsubscribeAndGet(req.context, req.params.listId, req.params.subscriptionId);
|
await subscriptions.unsubscribeByIdAndGet(req.context, req.params.listId, req.params.subscriptionId);
|
||||||
return res.json();
|
return res.json();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ const confirmations = require('../models/confirmations');
|
||||||
const subscriptions = require('../models/subscriptions');
|
const subscriptions = require('../models/subscriptions');
|
||||||
const lists = require('../models/lists');
|
const lists = require('../models/lists');
|
||||||
const fields = require('../models/fields');
|
const fields = require('../models/fields');
|
||||||
|
const shares = require('../models/shares');
|
||||||
const settings = require('../models/settings');
|
const settings = require('../models/settings');
|
||||||
const _ = require('../lib/translate')._;
|
const _ = require('../lib/translate')._;
|
||||||
const contextHelpers = require('../lib/context-helpers');
|
const contextHelpers = require('../lib/context-helpers');
|
||||||
|
@ -127,32 +128,40 @@ async function getMjmlTemplate(template) {
|
||||||
return renderer;
|
return renderer;
|
||||||
}
|
}
|
||||||
|
|
||||||
function captureFlashMessages(req, res) {
|
async function captureFlashMessages(res) {
|
||||||
return new Promise((resolve, reject) => {
|
const renderAsync = bluebird.promisify(res.render.bind(res));
|
||||||
res.render('subscription/capture-flash-messages', { layout: null }, (err, flash) => {
|
return await renderAsync('subscription/capture-flash-messages', { layout: null });
|
||||||
reject(err);
|
|
||||||
resolve(flash);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
router.getAsync('/confirm/subscribe/:cid', async (req, res) => {
|
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 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 data = confirmation.data;
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
cid: req.params.cid,
|
|
||||||
ip: confirmation.ip,
|
ip: confirmation.ip,
|
||||||
country: geoip.lookupCountry(confirmation.ip) || null
|
country: geoip.lookupCountry(confirmation.ip) || null,
|
||||||
|
replaceOfUnsubscribedAllowed: true
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const subscription = data.subscriptionData;
|
||||||
|
subscription.email = data.email;
|
||||||
subscription.status = SubscriptionStatus.SUBSCRIBED;
|
subscription.status = SubscriptionStatus.SUBSCRIBED;
|
||||||
|
|
||||||
|
try {
|
||||||
await subscriptions.create(contextHelpers.getAdminContext(), confirmation.list, subscription, meta);
|
await subscriptions.create(contextHelpers.getAdminContext(), confirmation.list, subscription, meta);
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof interoperableErrors.DuplicitEmailError) {
|
||||||
|
throw new interoperableErrors.DuplicitEmailError('Subscription already present'); // This is here to provide some meaningful error message.
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(subscription);
|
||||||
|
|
||||||
const list = await lists.getById(contextHelpers.getAdminContext(), confirmation.list);
|
const list = await lists.getById(contextHelpers.getAdminContext(), confirmation.list);
|
||||||
|
subscription.cid = meta.cid;
|
||||||
await mailHelpers.sendSubscriptionConfirmed(list, subscription.email, subscription);
|
await mailHelpers.sendSubscriptionConfirmed(list, subscription.email, subscription);
|
||||||
|
|
||||||
res.redirect('/subscription/' + encodeURIComponent(list.cid) + '/subscribed-notice');
|
res.redirect('/subscription/' + encodeURIComponent(list.cid) + '/subscribed-notice');
|
||||||
|
@ -187,15 +196,15 @@ router.getAsync('/confirm/unsubscribe/:cid', async (req, res) => {
|
||||||
|
|
||||||
|
|
||||||
router.getAsync('/:cid', passport.csrfProtection, async (req, res) => {
|
router.getAsync('/:cid', passport.csrfProtection, async (req, res) => {
|
||||||
const list = await lists.getByCid(req.params.cid);
|
const list = await lists.getByCid(contextHelpers.getAdminContext(), req.params.cid);
|
||||||
|
|
||||||
if (!list.publicSubscribe) {
|
if (!list.public_subscribe) {
|
||||||
throw new interoperableErrors.SubscriptionNotAllowedError('The list does not allow public subscriptions.');
|
shares.throwPermissionDenied();
|
||||||
}
|
}
|
||||||
|
|
||||||
const ucid = req.query.cid;
|
const ucid = req.query.cid;
|
||||||
|
|
||||||
const data = {};
|
const data = req.query;
|
||||||
data.layout = 'subscription/layout';
|
data.layout = 'subscription/layout';
|
||||||
data.title = list.name;
|
data.title = list.name;
|
||||||
data.cid = list.cid;
|
data.cid = list.cid;
|
||||||
|
@ -203,10 +212,14 @@ router.getAsync('/:cid', passport.csrfProtection, async (req, res) => {
|
||||||
|
|
||||||
let subscription;
|
let subscription;
|
||||||
if (ucid) {
|
if (ucid) {
|
||||||
subscription = await subscriptions.getById(contextHelpers.getAdminContext(), list.id, ucid, false);
|
subscription = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, ucid, false);
|
||||||
|
|
||||||
|
if (subscription) {
|
||||||
|
data.email = subscription.email;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data.customFields = fields.getRow(contextHelpers.getAdminContext(), list.id, subscription);
|
data.customFields = await fields.getRow(contextHelpers.getAdminContext(), list.id, subscription);
|
||||||
data.useEditor = true;
|
data.useEditor = true;
|
||||||
|
|
||||||
const configItems = await settings.get(['pgpPrivateKey', 'defaultAddress', 'defaultPostaddress']);
|
const configItems = await settings.get(['pgpPrivateKey', 'defaultAddress', 'defaultPostaddress']);
|
||||||
|
@ -227,7 +240,8 @@ router.getAsync('/:cid', passport.csrfProtection, async (req, res) => {
|
||||||
data.needsJsWarning = true;
|
data.needsJsWarning = true;
|
||||||
data.flashMessages = await captureFlashMessages(res);
|
data.flashMessages = await captureFlashMessages(res);
|
||||||
|
|
||||||
res.send(htmlRenderer(data));
|
const result = htmlRenderer(data);
|
||||||
|
res.send(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@ -241,7 +255,7 @@ router.getAsync('/:cid/widget', cors(corsOptions), async (req, res) => {
|
||||||
return res.status(200).json(cached);
|
return res.status(200).json(cached);
|
||||||
}
|
}
|
||||||
|
|
||||||
const list = await lists.getByCid(req.params.cid);
|
const list = await lists.getByCid(contextHelpers.getAdminContext(), req.params.cid);
|
||||||
|
|
||||||
const configItems = await settings.get(['serviceUrl', 'pgpPrivateKey']);
|
const configItems = await settings.get(['serviceUrl', 'pgpPrivateKey']);
|
||||||
|
|
||||||
|
@ -250,7 +264,7 @@ router.getAsync('/:cid/widget', cors(corsOptions), async (req, res) => {
|
||||||
cid: list.cid,
|
cid: list.cid,
|
||||||
serviceUrl: configItems.serviceUrl,
|
serviceUrl: configItems.serviceUrl,
|
||||||
hasPubkey: !!configItems.pgpPrivateKey,
|
hasPubkey: !!configItems.pgpPrivateKey,
|
||||||
customFields: fields.getRow(contextHelpers.getAdminContext(), list.id),
|
customFields: await fields.getRow(contextHelpers.getAdminContext(), list.id),
|
||||||
template: {},
|
template: {},
|
||||||
layout: null,
|
layout: null,
|
||||||
};
|
};
|
||||||
|
@ -293,11 +307,13 @@ router.postAsync('/:cid/subscribe', passport.parseForm, corsOrCsrfProtection, as
|
||||||
|
|
||||||
const emailErr = await tools.validateEmail(email);
|
const emailErr = await tools.validateEmail(email);
|
||||||
if (emailErr) {
|
if (emailErr) {
|
||||||
|
const errMsg = tools.validateEmailGetMessage(emailErr, email);
|
||||||
|
|
||||||
if (req.xhr) {
|
if (req.xhr) {
|
||||||
throw new Error(emailErr.message);
|
throw new Error(errMsg);
|
||||||
}
|
}
|
||||||
|
|
||||||
req.flash('danger', emailErr.message);
|
req.flash('danger', errMsg);
|
||||||
return res.redirect('/subscription/' + encodeURIComponent(req.params.cid) + '?' + tools.queryParams(req.body));
|
return res.redirect('/subscription/' + encodeURIComponent(req.params.cid) + '?' + tools.queryParams(req.body));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -310,20 +326,20 @@ router.postAsync('/:cid/subscribe', passport.parseForm, corsOrCsrfProtection, as
|
||||||
let addressTest = !req.body.address;
|
let addressTest = !req.body.address;
|
||||||
let testsPass = subTimeTest && addressTest;
|
let testsPass = subTimeTest && addressTest;
|
||||||
|
|
||||||
const list = await lists.getByCid(req.params.cid);
|
const list = await lists.getByCid(contextHelpers.getAdminContext(), req.params.cid);
|
||||||
|
|
||||||
if (!list.publicSubscribe) {
|
if (!list.public_subscribe) {
|
||||||
throw new interoperableErrors.SubscriptionNotAllowedError('The list does not allow public subscriptions.');
|
shares.throwPermissionDenied();
|
||||||
}
|
}
|
||||||
|
|
||||||
let subscriptionData = {};
|
const subscriptionData = {};
|
||||||
Object.keys(req.body).forEach(key => {
|
Object.keys(req.body).forEach(key => {
|
||||||
if (key !== 'email' && key.charAt(0) !== '_') {
|
if (key !== 'email' && key.charAt(0) !== '_') {
|
||||||
subscriptionData[key] = (req.body[key] || '').toString().trim();
|
subscriptionData[key] = (req.body[key] || '').toString().trim();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const subscription = subscriptions.getByEmail(list.id, email, false)
|
const subscription = await subscriptions.getByEmail(contextHelpers.getAdminContext(), list.id, email, false);
|
||||||
|
|
||||||
if (subscription && subscription.status === SubscriptionStatus.SUBSCRIBED) {
|
if (subscription && subscription.status === SubscriptionStatus.SUBSCRIBED) {
|
||||||
await mailHelpers.sendAlreadySubscribed(list, email, subscription);
|
await mailHelpers.sendAlreadySubscribed(list, email, subscription);
|
||||||
|
@ -353,33 +369,36 @@ router.postAsync('/:cid/subscribe', passport.parseForm, corsOrCsrfProtection, as
|
||||||
});
|
});
|
||||||
|
|
||||||
router.getAsync('/:lcid/manage/:ucid', passport.csrfProtection, async (req, res) => {
|
router.getAsync('/:lcid/manage/:ucid', passport.csrfProtection, async (req, res) => {
|
||||||
const list = await lists.getByCid(req.params.lcid);
|
const list = await lists.getByCid(contextHelpers.getAdminContext(), req.params.lcid);
|
||||||
const subscription = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, req.params.ucid, false);
|
const subscription = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, req.params.ucid, false);
|
||||||
|
|
||||||
if (!subscription || subscription.status !== SubscriptionStatus.SUBSCRIBED) {
|
if (!subscription || subscription.status !== SubscriptionStatus.SUBSCRIBED) {
|
||||||
throw new interoperableErrors.NotFoundError('Subscription not found in this list');
|
throw new interoperableErrors.NotFoundError('Subscription not found in this list');
|
||||||
}
|
}
|
||||||
|
|
||||||
subscription.lcid = req.params.lcid;
|
const data = {};
|
||||||
subscription.title = list.name;
|
data.email = subscription.email;
|
||||||
subscription.csrfToken = req.csrfToken();
|
data.cid = subscription.cid;
|
||||||
subscription.layout = 'subscription/layout';
|
data.lcid = req.params.lcid;
|
||||||
|
data.title = list.name;
|
||||||
|
data.csrfToken = req.csrfToken();
|
||||||
|
data.layout = 'data/layout';
|
||||||
|
|
||||||
subscription.customFields = await fields.getRow(contextHelpers.getAdminContext(), list.id, subscription);
|
data.customFields = await fields.getRow(contextHelpers.getAdminContext(), list.id, subscription);
|
||||||
|
|
||||||
subscription.useEditor = true;
|
data.useEditor = true;
|
||||||
|
|
||||||
const configItems = await settings.get(['pgpPrivateKey', 'defaultAddress', 'defaultPostaddress']);
|
const configItems = await settings.get(['pgpPrivateKey', 'defaultAddress', 'defaultPostaddress']);
|
||||||
subscription.hasPubkey = !!configItems.pgpPrivateKey;
|
data.hasPubkey = !!configItems.pgpPrivateKey;
|
||||||
subscription.defaultAddress = configItems.defaultAddress;
|
data.defaultAddress = configItems.defaultAddress;
|
||||||
subscription.defaultPostaddress = configItems.defaultPostaddress;
|
data.defaultPostaddress = configItems.defaultPostaddress;
|
||||||
|
|
||||||
subscription.template = {
|
data.template = {
|
||||||
template: 'subscription/web-manage.mjml.hbs',
|
template: 'subscription/web-manage.mjml.hbs',
|
||||||
layout: 'subscription/layout.mjml.hbs'
|
layout: 'subscription/layout.mjml.hbs'
|
||||||
};
|
};
|
||||||
|
|
||||||
await injectCustomFormData(req.query.fid || list.default_form, 'subscription/web-manage', subscription);
|
await injectCustomFormData(req.query.fid || list.default_form, 'data/web-manage', data);
|
||||||
|
|
||||||
const htmlRenderer = await getMjmlTemplate(data.template);
|
const htmlRenderer = await getMjmlTemplate(data.template);
|
||||||
|
|
||||||
|
@ -392,20 +411,23 @@ router.getAsync('/:lcid/manage/:ucid', passport.csrfProtection, async (req, res)
|
||||||
});
|
});
|
||||||
|
|
||||||
router.postAsync('/:lcid/manage', passport.parseForm, passport.csrfProtection, async (req, res) => {
|
router.postAsync('/:lcid/manage', passport.parseForm, passport.csrfProtection, async (req, res) => {
|
||||||
const list = await lists.getByCid(req.params.lcid);
|
const list = await lists.getByCid(contextHelpers.getAdminContext(), req.params.lcid);
|
||||||
const status = await subscriptions.getStatusByCid(contextHelpers.getAdminContext(), list.id, req.body.cid);
|
|
||||||
|
|
||||||
if (status !== SubscriptionStatus.SUBSCRIBED) {
|
try {
|
||||||
|
await subscriptions.updateManagedUngrouped(contextHelpers.getAdminContext(), list.id, req.body);
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof interoperableErrors.NotFoundError) {
|
||||||
throw new interoperableErrors.NotFoundError('Subscription not found in this list');
|
throw new interoperableErrors.NotFoundError('Subscription not found in this list');
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await subscriptions.updateManagedUngrouped(contextHelpers.getAdminContext(), list.id, req.body)
|
|
||||||
|
|
||||||
res.redirect('/subscription/' + encodeURIComponent(req.params.lcid) + '/updated-notice');
|
res.redirect('/subscription/' + encodeURIComponent(req.params.lcid) + '/updated-notice');
|
||||||
});
|
});
|
||||||
|
|
||||||
router.getAsync('/:lcid/manage-address/:ucid', passport.csrfProtection, async (req, res) => {
|
router.getAsync('/:lcid/manage-address/:ucid', passport.csrfProtection, async (req, res) => {
|
||||||
const list = await lists.getByCid(req.params.lcid);
|
const list = await lists.getByCid(contextHelpers.getAdminContext(), req.params.lcid);
|
||||||
const subscription = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, req.params.ucid, false);
|
const subscription = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, req.params.ucid, false);
|
||||||
|
|
||||||
if (!subscription || subscription.status !== SubscriptionStatus.SUBSCRIBED) {
|
if (!subscription || subscription.status !== SubscriptionStatus.SUBSCRIBED) {
|
||||||
|
@ -414,18 +436,21 @@ router.getAsync('/:lcid/manage-address/:ucid', passport.csrfProtection, async (r
|
||||||
|
|
||||||
const configItems = await settings.get(['defaultAddress', 'defaultPostaddress']);
|
const configItems = await settings.get(['defaultAddress', 'defaultPostaddress']);
|
||||||
|
|
||||||
subscription.lcid = req.params.lcid;
|
const data = {};
|
||||||
subscription.title = list.name;
|
data.email = subscription.email;
|
||||||
subscription.csrfToken = req.csrfToken();
|
data.cid = subscription.cid;
|
||||||
subscription.defaultAddress = configItems.defaultAddress;
|
data.lcid = req.params.lcid;
|
||||||
subscription.defaultPostaddress = configItems.defaultPostaddress;
|
data.title = list.name;
|
||||||
|
data.csrfToken = req.csrfToken();
|
||||||
|
data.defaultAddress = configItems.defaultAddress;
|
||||||
|
data.defaultPostaddress = configItems.defaultPostaddress;
|
||||||
|
|
||||||
subscription.template = {
|
data.template = {
|
||||||
template: 'subscription/web-manage-address.mjml.hbs',
|
template: 'subscription/web-manage-address.mjml.hbs',
|
||||||
layout: 'subscription/layout.mjml.hbs'
|
layout: 'subscription/layout.mjml.hbs'
|
||||||
};
|
};
|
||||||
|
|
||||||
await injectCustomFormData(req.query.fid || list.default_form, 'subscription/web-manage-address', subscription);
|
await injectCustomFormData(req.query.fid || list.default_form, 'data/web-manage-address', subscription);
|
||||||
|
|
||||||
const htmlRenderer = await getMjmlTemplate(data.template);
|
const htmlRenderer = await getMjmlTemplate(data.template);
|
||||||
|
|
||||||
|
@ -439,13 +464,13 @@ router.getAsync('/:lcid/manage-address/:ucid', passport.csrfProtection, async (r
|
||||||
|
|
||||||
|
|
||||||
router.postAsync('/:lcid/manage-address', passport.parseForm, passport.csrfProtection, async (req, res) => {
|
router.postAsync('/:lcid/manage-address', passport.parseForm, passport.csrfProtection, async (req, res) => {
|
||||||
const list = await lists.getByCid(req.params.lcid);
|
const list = await lists.getByCid(contextHelpers.getAdminContext(), req.params.lcid);
|
||||||
|
|
||||||
const emailNew = (req.body['email-new'] || '').toString().trim();
|
const emailNew = (req.body['email-new'] || '').toString().trim();
|
||||||
|
|
||||||
const subscription = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, req.body.cid, false);
|
const subscription = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, req.body.cid, false);
|
||||||
|
|
||||||
if (status !== SubscriptionStatus.SUBSCRIBED) {
|
if (subscription.status !== SubscriptionStatus.SUBSCRIBED) {
|
||||||
throw new interoperableErrors.NotFoundError('Subscription not found in this list');
|
throw new interoperableErrors.NotFoundError('Subscription not found in this list');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -455,7 +480,9 @@ router.postAsync('/:lcid/manage-address', passport.parseForm, passport.csrfProte
|
||||||
} else {
|
} else {
|
||||||
const emailErr = await tools.validateEmail(emailNew);
|
const emailErr = await tools.validateEmail(emailNew);
|
||||||
if (emailErr) {
|
if (emailErr) {
|
||||||
req.flash('danger', emailErr.message);
|
const errMsg = tools.validateEmailGetMessage(emailErr, email);
|
||||||
|
|
||||||
|
req.flash('danger', errMsg);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
const newSubscription = await subscriptions.getByEmail(contextHelpers.getAdminContext(), list.id, emailNew, false);
|
const newSubscription = await subscriptions.getByEmail(contextHelpers.getAdminContext(), list.id, emailNew, false);
|
||||||
|
@ -463,8 +490,13 @@ router.postAsync('/:lcid/manage-address', passport.parseForm, passport.csrfProte
|
||||||
if (newSubscription && newSubscription.status === SubscriptionStatus.SUBSCRIBED) {
|
if (newSubscription && newSubscription.status === SubscriptionStatus.SUBSCRIBED) {
|
||||||
await mailHelpers.sendAlreadySubscribed(list, emailNew, subscription);
|
await mailHelpers.sendAlreadySubscribed(list, emailNew, subscription);
|
||||||
} else {
|
} else {
|
||||||
|
const data = {
|
||||||
|
subscriptionId: subscription.id,
|
||||||
|
emailNew
|
||||||
|
};
|
||||||
|
|
||||||
const confirmCid = await confirmations.addConfirmation(list.id, 'change-address', req.ip, data);
|
const confirmCid = await confirmations.addConfirmation(list.id, 'change-address', req.ip, data);
|
||||||
await mailHelpers.sendConfirmAddressChange(list, emailNew, confirmCid, subscription, sendWebResponse);
|
await mailHelpers.sendConfirmAddressChange(list, emailNew, confirmCid, subscription);
|
||||||
}
|
}
|
||||||
|
|
||||||
req.flash('info', _('An email with further instructions has been sent to the provided address'));
|
req.flash('info', _('An email with further instructions has been sent to the provided address'));
|
||||||
|
@ -476,7 +508,7 @@ router.postAsync('/:lcid/manage-address', passport.parseForm, passport.csrfProte
|
||||||
|
|
||||||
|
|
||||||
router.getAsync('/:lcid/unsubscribe/:ucid', passport.csrfProtection, async (req, res) => {
|
router.getAsync('/:lcid/unsubscribe/:ucid', passport.csrfProtection, async (req, res) => {
|
||||||
const list = await lists.getByCid(req.params.lcid);
|
const list = await lists.getByCid(contextHelpers.getAdminContext(), req.params.lcid);
|
||||||
|
|
||||||
const configItems = await settings.get(['defaultAddress', 'defaultPostaddress']);
|
const configItems = await settings.get(['defaultAddress', 'defaultPostaddress']);
|
||||||
|
|
||||||
|
@ -486,8 +518,8 @@ router.getAsync('/:lcid/unsubscribe/:ucid', passport.csrfProtection, async (req,
|
||||||
handleUnsubscribe(list, req.params.ucid, autoUnsubscribe, req.query.c, req.ip, res, next);
|
handleUnsubscribe(list, req.params.ucid, autoUnsubscribe, req.query.c, req.ip, res, next);
|
||||||
|
|
||||||
} else if (req.query.formTest ||
|
} else if (req.query.formTest ||
|
||||||
list.unsubscriptionMode === lists.UnsubscriptionMode.ONE_STEP_WITH_FORM ||
|
list.unsubscription_mode === lists.UnsubscriptionMode.ONE_STEP_WITH_FORM ||
|
||||||
list.unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP_WITH_FORM) {
|
list.unsubscription_mode === lists.UnsubscriptionMode.TWO_STEP_WITH_FORM) {
|
||||||
|
|
||||||
const subscription = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, req.params.ucid, false);
|
const subscription = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, req.params.ucid, false);
|
||||||
|
|
||||||
|
@ -495,20 +527,22 @@ router.getAsync('/:lcid/unsubscribe/:ucid', passport.csrfProtection, async (req,
|
||||||
throw new interoperableErrors.NotFoundError('Subscription not found in this list');
|
throw new interoperableErrors.NotFoundError('Subscription not found in this list');
|
||||||
}
|
}
|
||||||
|
|
||||||
subscription.lcid = req.params.lcid;
|
const data = {};
|
||||||
subscription.ucid = req.params.ucid;
|
data.email = subscription.email;
|
||||||
subscription.title = list.name;
|
data.lcid = req.params.lcid;
|
||||||
subscription.csrfToken = req.csrfToken();
|
data.ucid = req.params.ucid;
|
||||||
subscription.campaign = req.query.c;
|
data.title = list.name;
|
||||||
subscription.defaultAddress = configItems.defaultAddress;
|
data.csrfToken = req.csrfToken();
|
||||||
subscription.defaultPostaddress = configItems.defaultPostaddress;
|
data.campaign = req.query.c;
|
||||||
|
data.defaultAddress = configItems.defaultAddress;
|
||||||
|
data.defaultPostaddress = configItems.defaultPostaddress;
|
||||||
|
|
||||||
subscription.template = {
|
data.template = {
|
||||||
template: 'subscription/web-unsubscribe.mjml.hbs',
|
template: 'subscription/web-unsubscribe.mjml.hbs',
|
||||||
layout: 'subscription/layout.mjml.hbs'
|
layout: 'subscription/layout.mjml.hbs'
|
||||||
};
|
};
|
||||||
|
|
||||||
await injectCustomFormData(req.query.fid || list.default_form, 'subscription/web-unsubscribe', subscription);
|
await injectCustomFormData(req.query.fid || list.default_form, 'subscription/web-unsubscribe', data);
|
||||||
|
|
||||||
const htmlRenderer = await getMjmlTemplate(data.template);
|
const htmlRenderer = await getMjmlTemplate(data.template);
|
||||||
|
|
||||||
|
@ -526,7 +560,7 @@ router.getAsync('/:lcid/unsubscribe/:ucid', passport.csrfProtection, async (req,
|
||||||
|
|
||||||
|
|
||||||
router.postAsync('/:lcid/unsubscribe', passport.parseForm, passport.csrfProtection, async (req, res) => {
|
router.postAsync('/:lcid/unsubscribe', passport.parseForm, passport.csrfProtection, async (req, res) => {
|
||||||
const list = await lists.getByCid(req.params.lcid);
|
const list = await lists.getByCid(contextHelpers.getAdminContext(), req.params.lcid);
|
||||||
|
|
||||||
const campaignCid = (req.body.campaign || '').toString().trim() || false;
|
const campaignCid = (req.body.campaign || '').toString().trim() || false;
|
||||||
|
|
||||||
|
@ -535,8 +569,8 @@ router.postAsync('/:lcid/unsubscribe', passport.parseForm, passport.csrfProtecti
|
||||||
|
|
||||||
|
|
||||||
async function handleUnsubscribe(list, subscriptionCid, autoUnsubscribe, campaignCid, ip, res) {
|
async function handleUnsubscribe(list, subscriptionCid, autoUnsubscribe, campaignCid, ip, res) {
|
||||||
if ((list.unsubscriptionMode === lists.UnsubscriptionMode.ONE_STEP || list.unsubscriptionMode === lists.UnsubscriptionMode.ONE_STEP_WITH_FORM) ||
|
if ((list.unsubscription_mode === lists.UnsubscriptionMode.ONE_STEP || list.unsubscription_mode === lists.UnsubscriptionMode.ONE_STEP_WITH_FORM) ||
|
||||||
(autoUnsubscribe && (list.unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP || list.unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP_WITH_FORM)) ) {
|
(autoUnsubscribe && (list.unsubscription_mode === lists.UnsubscriptionMode.TWO_STEP || list.unsubscription_mode === lists.UnsubscriptionMode.TWO_STEP_WITH_FORM)) ) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const subscription = await subscriptions.unsubscribeByCidAndGet(contextHelpers.getAdminContext(), list.id, subscriptionCid, campaignCid);
|
const subscription = await subscriptions.unsubscribeByCidAndGet(contextHelpers.getAdminContext(), list.id, subscriptionCid, campaignCid);
|
||||||
|
@ -558,7 +592,7 @@ async function handleUnsubscribe(list, subscriptionCid, autoUnsubscribe, campaig
|
||||||
throw new interoperableErrors.NotFoundError('Subscription not found in this list');
|
throw new interoperableErrors.NotFoundError('Subscription not found in this list');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (list.unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP || list.unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP_WITH_FORM) {
|
if (list.unsubscription_mode === lists.UnsubscriptionMode.TWO_STEP || list.unsubscription_mode === lists.UnsubscriptionMode.TWO_STEP_WITH_FORM) {
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
subscriptionCid,
|
subscriptionCid,
|
||||||
|
@ -638,7 +672,7 @@ router.postAsync('/publickey', passport.parseForm, async (req, res) => {
|
||||||
|
|
||||||
|
|
||||||
async function webNotice(type, req, res) {
|
async function webNotice(type, req, res) {
|
||||||
const list = await lists.getByCid(req.params.cid);
|
const list = await lists.getByCid(contextHelpers.getAdminContext(), req.params.cid);
|
||||||
|
|
||||||
const configItems = await settings.get(['defaultHomepage', 'serviceUrl', 'defaultAddress', 'defaultPostaddress', 'adminEmail']);
|
const configItems = await settings.get(['defaultHomepage', 'serviceUrl', 'defaultAddress', 'defaultPostaddress', 'adminEmail']);
|
||||||
|
|
||||||
|
|
|
@ -105,13 +105,6 @@ class InvalidConfirmationForUnsubscriptionError extends InteroperableError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SubscriptionNotAllowedError extends InteroperableError {
|
|
||||||
constructor(msg, data) {
|
|
||||||
super('SubscriptionNotAllowedError', msg, data);
|
|
||||||
this.status = 403;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const errorTypes = {
|
const errorTypes = {
|
||||||
InteroperableError,
|
InteroperableError,
|
||||||
NotLoggedInError,
|
NotLoggedInError,
|
||||||
|
@ -129,8 +122,7 @@ const errorTypes = {
|
||||||
PermissionDeniedError,
|
PermissionDeniedError,
|
||||||
InvalidConfirmationForSubscriptionError,
|
InvalidConfirmationForSubscriptionError,
|
||||||
InvalidConfirmationForAddressChangeError,
|
InvalidConfirmationForAddressChangeError,
|
||||||
InvalidConfirmationForUnsubscriptionError,
|
InvalidConfirmationForUnsubscriptionError
|
||||||
SubscriptionNotAllowedError
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function deserialize(errorObj) {
|
function deserialize(errorObj) {
|
||||||
|
|
24
shared/package-lock.json
generated
Normal file
24
shared/package-lock.json
generated
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"name": "mailtrain-shared",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"requires": true,
|
||||||
|
"dependencies": {
|
||||||
|
"moment": {
|
||||||
|
"version": "2.20.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/moment/-/moment-2.20.1.tgz",
|
||||||
|
"integrity": "sha512-Yh9y73JRljxW5QxN08Fner68eFLxM5ynNOAw2LbIB1YAGeQzZT8QFSUvkAz609Zf+IHhhaUxqZK8dG3W/+HEvg=="
|
||||||
|
},
|
||||||
|
"moment-timezone": {
|
||||||
|
"version": "0.5.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.14.tgz",
|
||||||
|
"integrity": "sha1-TrOP+VOLgBCLpGekWPPtQmjM/LE=",
|
||||||
|
"requires": {
|
||||||
|
"moment": "2.20.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"owasp-password-strength-test": {
|
||||||
|
"version": "github:bures/owasp-password-strength-test#50bfcf0035b1468b9d03a00eaf561d4fed4973eb"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
20
shared/package.json
Normal file
20
shared/package.json
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"name": "mailtrain-shared",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Self hosted email newsletter app - shared lib",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git://github.com/Mailtrain-org/mailtrain.git"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "GPL-3.0",
|
||||||
|
"homepage": "https://mailtrain.org/",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=5.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"moment": "^2.18.1",
|
||||||
|
"moment-timezone": "^0.5.13",
|
||||||
|
"owasp-password-strength-test": "github:bures/owasp-password-strength-test"
|
||||||
|
}
|
||||||
|
}
|
|
@ -181,7 +181,7 @@
|
||||||
|
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<p class="text-muted">© 2016 Kreata OÜ <a href="https://mailtrain.org">Mailtrain.org</a>, <a href="mailto:info@mailtrain.org">info@mailtrain.org</a>. <a href="https://github.com/Mailtrain-org/mailtrain">{{#translate}}Source on GitHub{{/translate}}</a></p>
|
<p class="text-muted">© 2018 <a href="https://mailtrain.org">Mailtrain.org</a>, <a href="mailto:info@mailtrain.org">info@mailtrain.org</a>. <a href="https://github.com/Mailtrain-org/mailtrain">{{#translate}}Source on GitHub{{/translate}}</a></p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue