Basic support for Mosaico-based email templates.

This commit is contained in:
Tomas Bures 2018-04-02 11:58:32 +02:00
parent b5cdf57f72
commit 7b5642e911
38 changed files with 1271 additions and 751 deletions

View file

@ -133,7 +133,8 @@ async function createFiles(context, type, entityId, files, dontReplace = false)
originalName: originalName,
size: file.size,
type: file.mimetype,
url: `/files/${type}/${entityId}/${file.filename}`
url: `/files/${type}/${entityId}/${file.filename}`,
thumbnailUrl: `/files/${type}/${entityId}/${file.filename}` // TODO - use smaller thumbnails
});
if (existingNameMap.has(originalName)) {
@ -145,10 +146,10 @@ async function createFiles(context, type, entityId, files, dontReplace = false)
}
const originalNameArray = Array.from(originalNameSet);
await knex(getFilesTable(type)).where('entity', entityId).whereIn('originalname', originalNameArray).del();
await tx(getFilesTable(type)).where('entity', entityId).whereIn('originalname', originalNameArray).del();
if (fileEntities) {
await knex(getFilesTable(type)).insert(fileEntities);
await tx(getFilesTable(type)).insert(fileEntities);
}
});

View file

@ -7,6 +7,7 @@ const { enforce } = require('../lib/helpers');
const dtHelpers = require('../lib/dt-helpers');
const permissions = require('../lib/permissions');
const interoperableErrors = require('../shared/interoperable-errors');
const log = require('npmlog');
// TODO: This would really benefit from some permission cache connected to rebuildPermissions
// A bit of the problem is that the cache would have to expunged as the result of other processes modifying entites/permissions
@ -418,14 +419,26 @@ function checkGlobalPermission(context, requiredOperations) {
return false;
}
if (context.user.admin) { // This handles the getAdminContext() case
return true;
}
if (typeof requiredOperations === 'string') {
requiredOperations = [ requiredOperations ];
}
if (context.user.restrictedAccessHandler) {
log.verbose('check global permissions with restrictedAccessHandler -- requiredOperations: ' + requiredOperations);
const allowedPerms = context.user.restrictedAccessHandler.globalPermissions;
if (allowedPerms) {
requiredOperations = requiredOperations.filter(perm => allowedPerms.has(perm));
}
}
if (requiredOperations.length === 0) {
return false;
}
if (context.user.admin) { // This handles the getAdminContext() case
return true;
}
const roleSpec = config.roles.global[context.user.role];
let success = false;
if (roleSpec) {
@ -453,6 +466,24 @@ async function _checkPermissionTx(tx, context, entityTypeId, entityId, requiredO
const entityType = permissions.getEntityType(entityTypeId);
if (typeof requiredOperations === 'string') {
requiredOperations = [ requiredOperations ];
}
if (context.user.restrictedAccessHandler) {
log.verbose('check permissions with restrictedAccessHandler -- entityTypeId: ' + entityTypeId + ' entityId: ' + entityId + ' requiredOperations: ' + requiredOperations);
if (context.user.restrictedAccessHandler.permissions && context.user.restrictedAccessHandler.permissions[entityTypeId]) {
const allowedPerms = context.user.restrictedAccessHandler.permissions[entityTypeId][entityId];
if (allowedPerms) {
requiredOperations = requiredOperations.filter(perm => allowedPerms.has(perm));
}
}
}
if (requiredOperations.length === 0) {
return false;
}
if (context.user.admin) { // This handles the getAdminContext() case. In this case we don't check the permission, but just the existence.
const existsQuery = tx(entityType.entitiesTable);
@ -465,10 +496,6 @@ async function _checkPermissionTx(tx, context, entityTypeId, entityId, requiredO
return !!exists;
} else {
if (typeof requiredOperations === 'string') {
requiredOperations = [ requiredOperations ];
}
const permsQuery = tx(entityType.permissionsTable)
.where('user', context.user.id)
.whereIn('operation', requiredOperations);
@ -564,7 +591,6 @@ async function getPermissionsTx(tx, context, entityTypeId, entityId) {
return rows.map(x => x.operation);
}
module.exports = {
listByEntityDTAjax,
listByUserDTAjax,

View file

@ -19,6 +19,8 @@ async function getById(context, id) {
return await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'template', id, 'view');
const entity = await tx('templates').where('id', id).first();
entity.data = JSON.parse(entity.data);
entity.permissions = await shares.getPermissionsTx(tx, context, 'template', id);
return entity;
});
@ -34,9 +36,16 @@ async function listDTAjax(context, params) {
);
}
async function _validateAndPreprocess(tx, entity, isCreate) {
entity.data = JSON.stringify(entity.data);
}
async function create(context, entity) {
return await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'namespace', entity.namespace, 'createTemplate');
await _validateAndPreprocess(tx, entity, true);
await namespaceHelpers.validateEntity(tx, entity);
const ids = await tx('templates').insert(filterObject(entity, allowedKeys));
@ -57,11 +66,15 @@ async function updateWithConsistencyCheck(context, entity) {
throw new interoperableErrors.NotFoundError();
}
existing.data = JSON.parse(existing.data);
const existingHash = hash(existing);
if (existingHash !== entity.originalHash) {
throw new interoperableErrors.ChangedError();
}
await _validateAndPreprocess(tx, entity, false);
await namespaceHelpers.validateEntity(tx, entity);
await namespaceHelpers.validateMove(context, entity, existing, 'template', 'createTemplate', 'delete');

View file

@ -8,7 +8,7 @@ const interoperableErrors = require('../shared/interoperable-errors');
const passwordValidator = require('../shared/password-validator')();
const dtHelpers = require('../lib/dt-helpers');
const tools = require('../lib/tools-async');
let crypto = require('crypto');
const crypto = require('crypto');
const settings = require('./settings');
const urllib = require('url');
const _ = require('../lib/translate')._;
@ -43,11 +43,7 @@ async function _getBy(context, key, value, extraColumns = []) {
const user = await knex('users').select(columns).where(key, value).first();
if (!user) {
if (context) {
shares.throwPermissionDenied();
} else {
throw new interoperableErrors.NotFoundError();
}
shares.throwPermissionDenied();
}
await shares.enforceEntityPermission(context, 'namespace', user.namespace, 'manageUsers');
@ -367,6 +363,50 @@ async function resetPassword(username, resetToken, password) {
}
const restrictedAccessTokenMethods = {};
const restrictedAccessTokens = new Map();
function registerRestrictedAccessTokenMethod(method, getHandlerFromParams) {
restrictedAccessTokenMethods[method] = getHandlerFromParams;
}
function getRestrictedAccessToken(context, method, params) {
const token = crypto.randomBytes(24).toString('hex').toLowerCase();
const tokenEntry = {
token,
userId: context.user.id,
handler: restrictedAccessTokenMethods[method](params),
expires: Date.now() + 120 * 1000
};
restrictedAccessTokens.set(token, tokenEntry);
return token;
}
async function getByRestrictedAccessToken(token) {
const now = Date.now();
for (const entry of restrictedAccessTokens.values()) {
if (entry.expires < now) {
restrictedAccessTokens.delete(entry.token);
}
}
const tokenEntry = restrictedAccessTokens.get(token);
if (tokenEntry) {
const user = await getById(contextHelpers.getAdminContext(), tokenEntry.userId);
user.restrictedAccessHandler = tokenEntry.handler;
return user;
} else {
shares.throwPermissionDenied();
}
}
module.exports = {
listDTAjax,
remove,
@ -382,5 +422,8 @@ module.exports = {
resetAccessToken,
sendPasswordReset,
isPasswordResetTokenValid,
resetPassword
resetPassword,
getByRestrictedAccessToken,
getRestrictedAccessToken,
registerRestrictedAccessTokenMethod
};