WiP updates
This commit is contained in:
parent
6706d93bc1
commit
4fce4b6f81
27 changed files with 763 additions and 85 deletions
|
@ -33,6 +33,7 @@ const mosaico = require('./routes/mosaico');
|
|||
const files = require('./routes/files');
|
||||
|
||||
const namespacesRest = require('./routes/rest/namespaces');
|
||||
const sendConfigurationsRest = require('./routes/rest/send-configurations');
|
||||
const usersRest = require('./routes/rest/users');
|
||||
const accountRest = require('./routes/rest/account');
|
||||
const reportTemplatesRest = require('./routes/rest/report-templates');
|
||||
|
@ -270,6 +271,7 @@ function createApp(trusted) {
|
|||
|
||||
// REST endpoints
|
||||
app.use('/rest', namespacesRest);
|
||||
app.use('/rest', sendConfigurationsRest);
|
||||
app.use('/rest', usersRest);
|
||||
app.use('/rest', accountRest);
|
||||
app.use('/rest', campaignsRest);
|
||||
|
|
|
@ -44,7 +44,7 @@ export default class API extends Component {
|
|||
const t = this.props.t;
|
||||
|
||||
const thisUrl = new URL();
|
||||
const serviceUrl = thisUrl.origin + '/';
|
||||
const serviceUrl = thisUrl.origin + '/'; // FIXME - use urls.js and getTrustedUrl
|
||||
const accessToken = this.state.accessToken || 'ACCESS_TOKEN';
|
||||
|
||||
let accessTokenMsg;
|
||||
|
|
27
client/src/lib/bootstrap-components.js
vendored
27
client/src/lib/bootstrap-components.js
vendored
|
@ -123,6 +123,32 @@ class DropdownMenu extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
class DropdownMenuItem extends Component {
|
||||
static propTypes = {
|
||||
label: PropTypes.string,
|
||||
icon: PropTypes.string,
|
||||
className: PropTypes.string
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
|
||||
let className = 'dropdown';
|
||||
if (props.className) {
|
||||
className = className + ' ' + props.className;
|
||||
}
|
||||
|
||||
return (
|
||||
<li className={className}>
|
||||
<a href="#" className="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">{props.icon && <Icon icon={props.icon}/>}{props.label}{' '}<span className="caret"></span></a>
|
||||
<ul className="dropdown-menu">
|
||||
{props.children}
|
||||
</ul>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@withErrorHandling
|
||||
class ActionLink extends Component {
|
||||
static propTypes = {
|
||||
|
@ -261,6 +287,7 @@ class ModalDialog extends Component {
|
|||
export {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuItem,
|
||||
ActionLink,
|
||||
DismissibleAlert,
|
||||
ModalDialog,
|
||||
|
|
|
@ -417,6 +417,7 @@ class TextArea extends Component {
|
|||
static propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
placeholder: PropTypes.string,
|
||||
help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
format: PropTypes.string
|
||||
}
|
||||
|
@ -432,7 +433,7 @@ class TextArea extends Component {
|
|||
const htmlId = 'form_' + id;
|
||||
|
||||
return wrapInput(id, htmlId, owner, props.format, '', props.label, props.help,
|
||||
<textarea id={htmlId} value={owner.getFormValue(id) || ''} className="form-control" aria-describedby={htmlId + '_help'} onChange={evt => owner.updateFormValue(id, evt.target.value)}></textarea>
|
||||
<textarea id={htmlId} placeholder={props.placeholder} value={owner.getFormValue(id) || ''} className="form-control" aria-describedby={htmlId + '_help'} onChange={evt => owner.updateFormValue(id, evt.target.value)}></textarea>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -437,16 +437,17 @@ class NavButton extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
class DropdownLink extends Component {
|
||||
class MenuLink extends Component {
|
||||
static propTypes = {
|
||||
to: PropTypes.string
|
||||
to: PropTypes.string,
|
||||
className: PropTypes.string
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
|
||||
return (
|
||||
<li><Link to={props.to}>{props.children}</Link></li>
|
||||
<li className={props.className}><Link to={props.to}>{props.children}</Link></li>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -473,7 +474,7 @@ export {
|
|||
Title,
|
||||
Toolbar,
|
||||
NavButton,
|
||||
DropdownLink,
|
||||
MenuLink,
|
||||
withPageHelpers,
|
||||
requiresAuthenticatedUser
|
||||
};
|
|
@ -41,7 +41,7 @@ export class UntrustedContentHost extends Component {
|
|||
}
|
||||
|
||||
isInitialized() {
|
||||
return !!this.accessToken;
|
||||
return !!this.accessToken && !!this.props.contentProps;
|
||||
}
|
||||
|
||||
receiveMessage(evt) {
|
||||
|
@ -73,7 +73,6 @@ export class UntrustedContentHost extends Component {
|
|||
const msgId = this.rpcCounter;
|
||||
|
||||
this.sendMessage('rpcRequest', {
|
||||
method,
|
||||
params,
|
||||
msgId
|
||||
});
|
||||
|
@ -126,6 +125,10 @@ export class UntrustedContentHost extends Component {
|
|||
this.handleUpdate();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.handleUpdate();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearTimeout(this.refreshAccessTokenTimeout);
|
||||
window.removeEventListener('message', this.receiveMessageHandler, false);
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import React, { Component } from 'react';
|
||||
import { translate } from 'react-i18next';
|
||||
import {DropdownMenu, Icon} from '../../lib/bootstrap-components';
|
||||
import { requiresAuthenticatedUser, withPageHelpers, Title, Toolbar, DropdownLink } from '../../lib/page';
|
||||
import { requiresAuthenticatedUser, withPageHelpers, Title, Toolbar, MenuLink } from '../../lib/page';
|
||||
import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handling';
|
||||
import { Table } from '../../lib/table';
|
||||
import axios from '../../lib/axios';
|
||||
|
@ -78,10 +78,10 @@ export default class List extends Component {
|
|||
{this.state.createPermitted &&
|
||||
<Toolbar>
|
||||
<DropdownMenu className="btn-primary" label={t('Create Report Template')}>
|
||||
<DropdownLink to="/reports/templates/create">{t('Blank')}</DropdownLink>
|
||||
<DropdownLink to="/reports/templates/create/subscribers-all">{t('All Subscribers')}</DropdownLink>
|
||||
<DropdownLink to="/reports/templates/create/subscribers-grouped">{t('Grouped Subscribers')}</DropdownLink>
|
||||
<DropdownLink to="/reports/templates/create/export-list-csv">{t('Export List as CSV')}</DropdownLink>
|
||||
<MenuLink to="/reports/templates/create">{t('Blank')}</MenuLink>
|
||||
<MenuLink to="/reports/templates/create/subscribers-all">{t('All Subscribers')}</MenuLink>
|
||||
<MenuLink to="/reports/templates/create/subscribers-grouped">{t('Grouped Subscribers')}</MenuLink>
|
||||
<MenuLink to="/reports/templates/create/export-list-csv">{t('Export List as CSV')}</MenuLink>
|
||||
</DropdownMenu>
|
||||
</Toolbar>
|
||||
}
|
||||
|
|
|
@ -15,12 +15,18 @@ import namespaces from './namespaces/root';
|
|||
import reports from './reports/root';
|
||||
import templates from './templates/root';
|
||||
import users from './users/root';
|
||||
import {Section} from "./lib/page";
|
||||
import sendConfigurations from './send-configurations/root';
|
||||
|
||||
import {
|
||||
MenuLink,
|
||||
Section
|
||||
} from "./lib/page";
|
||||
|
||||
import mailtrainConfig from 'mailtrainConfig';
|
||||
import Home from "./Home";
|
||||
import {
|
||||
ActionLink,
|
||||
DropdownMenuItem,
|
||||
Icon
|
||||
} from "./lib/bootstrap-components";
|
||||
import {Link} from "react-router-dom";
|
||||
|
@ -47,9 +53,9 @@ class Root extends Component {
|
|||
const link = entry.link || entry.externalLink;
|
||||
|
||||
if (link && path.startsWith(link)) {
|
||||
topLevelMenu.push(<li key={entryKey} className="active"><Link to={link}>{entry.title} <span className="sr-only">{t('(current)')}</span></Link></li>);
|
||||
topLevelMenu.push(<MenuLink key={entryKey} className="active" to={link}>{entry.title} <span className="sr-only">{t('(current)')}</span></MenuLink>);
|
||||
} else {
|
||||
topLevelMenu.push(<li key={entryKey}><Link to={link}>{entry.title}</Link></li>);
|
||||
topLevelMenu.push(<MenuLink key={entryKey} to={link}>{entry.title}</MenuLink>);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -63,51 +69,31 @@ class Root extends Component {
|
|||
<span className="icon-bar"></span>
|
||||
<span className="icon-bar"></span>
|
||||
</button>
|
||||
<Link className="navbar-brand" to="/"><i className="glyphicon glyphicon-envelope"></i> Mailtrain</Link>
|
||||
<Link className="navbar-brand" to="/"><Icon icon="envelope"/> Mailtrain</Link>
|
||||
</div>
|
||||
|
||||
{mailtrainConfig.isAuthenticated &&
|
||||
<div className="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
|
||||
<ul className="nav navbar-nav">
|
||||
{topLevelMenu}
|
||||
|
||||
<li className="dropdown">
|
||||
<a href="#" className="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">{t('Administration')}<span className="caret"></span></a>
|
||||
<ul className="dropdown-menu">
|
||||
<li>
|
||||
<Link to="/users"><Icon icon='cog'/> {t('Users')}</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/namespaces"><Icon icon='cog'/> {t('Namespaces')}</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/settings"><Icon icon='cog'/> {t('Settings')}</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/blacklist"><Icon icon='ban-circle'/> {t('Blacklist')}</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/account/api"><Icon icon='retweet'/> {t('API')}</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<DropdownMenuItem label={t('Administration')}>
|
||||
<MenuLink to="/users"><Icon icon='cog'/> {t('Users')}</MenuLink>
|
||||
<MenuLink to="/namespaces"><Icon icon='cog'/> {t('Namespaces')}</MenuLink>
|
||||
<MenuLink to="/settings"><Icon icon='cog'/> {t('Settings')}</MenuLink>
|
||||
<MenuLink to="/send-configurations"><Icon icon='cog'/> {t('Send Configurations')}</MenuLink>
|
||||
<MenuLink to="/blacklist"><Icon icon='ban-circle'/> {t('Blacklist')}</MenuLink>
|
||||
<MenuLink to="/account/api"><Icon icon='retweet'/> {t('API')}</MenuLink>
|
||||
</DropdownMenuItem>
|
||||
</ul>
|
||||
|
||||
|
||||
<ul className="nav navbar-nav navbar-right">
|
||||
<li className="dropdown">
|
||||
<a href="#" className="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
|
||||
<span className="glyphicon glyphicon-user" aria-hidden="true"></span> {mailtrainConfig.user.username}<span className="caret"></span>
|
||||
</a>
|
||||
<ul className="dropdown-menu">
|
||||
<li>
|
||||
<Link to="/account"><Icon icon='user'/> {t('Account')}</Link>
|
||||
</li>
|
||||
<li>
|
||||
<ActionLink onClickAsync={::self.logout}><Icon icon='log-out'/> {t('Log out')}</ActionLink>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<DropdownMenuItem label={mailtrainConfig.user.username} icon="user">
|
||||
<MenuLink to="/account"><Icon icon='user'/> {t('Account')}</MenuLink>
|
||||
<li>
|
||||
<ActionLink onClickAsync={::self.logout}><Icon icon='log-out'/> {t('Log out')}</ActionLink>
|
||||
</li>
|
||||
</DropdownMenuItem>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
|
@ -132,6 +118,7 @@ class Root extends Component {
|
|||
...users.getMenus(t),
|
||||
...blacklist.getMenus(t),
|
||||
...account.getMenus(t),
|
||||
...sendConfigurations.getMenus(t)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
201
client/src/send-configurations/CUD.js
Normal file
201
client/src/send-configurations/CUD.js
Normal file
|
@ -0,0 +1,201 @@
|
|||
'use strict';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translate, Trans } from 'react-i18next';
|
||||
import {requiresAuthenticatedUser, withPageHelpers, Title, NavButton} from '../lib/page'
|
||||
import {
|
||||
withForm,
|
||||
Form,
|
||||
FormSendMethod,
|
||||
InputField,
|
||||
TextArea,
|
||||
Dropdown,
|
||||
ButtonRow,
|
||||
Button,
|
||||
CheckBox,
|
||||
Fieldset
|
||||
} from '../lib/form';
|
||||
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
|
||||
import { validateNamespace, NamespaceSelect } from '../lib/namespace';
|
||||
import {DeleteModalDialog} from "../lib/modals";
|
||||
|
||||
import { getMailerTypes, mailerTypesOrder } from "./helpers";
|
||||
|
||||
import {MailerType} from "../../../shared/send-configurations";
|
||||
|
||||
@translate()
|
||||
@withForm
|
||||
@withPageHelpers
|
||||
@withErrorHandling
|
||||
@requiresAuthenticatedUser
|
||||
export default class CUD extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.mailerTypes = getMailerTypes(props.t);
|
||||
|
||||
this.typeOptions = [];
|
||||
for (const type of mailerTypesOrder) {
|
||||
this.typeOptions.push({
|
||||
key: type,
|
||||
label: this.mailerTypes[type].typeName
|
||||
});
|
||||
}
|
||||
|
||||
this.state = {};
|
||||
|
||||
this.initForm({
|
||||
onChangeBeforeValidation: {
|
||||
mailer_type: ::this.onMailerTypeChanged
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
action: PropTypes.string.isRequired,
|
||||
wizard: PropTypes.string,
|
||||
entity: PropTypes.object
|
||||
}
|
||||
|
||||
onMailerTypeChanged(mutState, key, oldType, type) {
|
||||
if (type) {
|
||||
this.mailerTypes[type].afterTypeChange(mutState);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.entity) {
|
||||
this.getFormValuesFromEntity(this.props.entity, data => {
|
||||
this.mailerTypes[data.type].afterLoad(data);
|
||||
});
|
||||
|
||||
} else {
|
||||
this.populateFormValues({
|
||||
name: '',
|
||||
description: '',
|
||||
namespace: mailtrainConfig.user.namespace,
|
||||
from_email_overridable: false,
|
||||
from_name_overridable: false,
|
||||
subject_overridable: false,
|
||||
mailer_type: MailerType.ZONE_MTA,
|
||||
...this.mailerTypes[MailerType.ZONE_MTA].initData()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
localValidateFormValues(state) {
|
||||
const t = this.props.t;
|
||||
|
||||
if (!state.getIn(['name', 'value'])) {
|
||||
state.setIn(['name', 'error'], t('Name must not be empty'));
|
||||
} else {
|
||||
state.setIn(['name', 'error'], null);
|
||||
}
|
||||
|
||||
if (!state.getIn(['mailer_type', 'value'])) {
|
||||
state.setIn(['mailer_type', 'error'], t('Mailer type must be selected'));
|
||||
} else {
|
||||
state.setIn(['mailer_type', 'error'], null);
|
||||
}
|
||||
|
||||
validateNamespace(t, state);
|
||||
}
|
||||
|
||||
async submitHandler() {
|
||||
const t = this.props.t;
|
||||
|
||||
let sendMethod, url;
|
||||
if (this.props.entity) {
|
||||
sendMethod = FormSendMethod.PUT;
|
||||
url = `/rest/send-configurations/${this.props.entity.id}`
|
||||
} else {
|
||||
sendMethod = FormSendMethod.POST;
|
||||
url = '/rest/send-configurations'
|
||||
}
|
||||
|
||||
this.disableForm();
|
||||
this.setFormStatusMessage('info', t('Saving ...'));
|
||||
|
||||
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
|
||||
this.mailerTypes[data.type].beforeSave(data);
|
||||
});
|
||||
|
||||
if (submitSuccessful) {
|
||||
this.navigateToWithFlashMessage('/send-configurations', 'success', t('Send configuration saved'));
|
||||
} else {
|
||||
this.enableForm();
|
||||
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const isEdit = !!this.props.entity;
|
||||
const canDelete = isEdit && this.props.entity.permissions.includes('delete');
|
||||
|
||||
const typeKey = this.getFormValue('mailer_type');
|
||||
let mailerForm = null;
|
||||
if (typeKey) {
|
||||
mailerForm = this.mailerTypes[typeKey].getForm(this);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{canDelete &&
|
||||
<DeleteModalDialog
|
||||
stateOwner={this}
|
||||
visible={this.props.action === 'delete'}
|
||||
deleteUrl={`/rest/send-configurations/${this.props.entity.id}`}
|
||||
cudUrl={`/send-configurations/${this.props.entity.id}/edit`}
|
||||
listUrl="/send-configurations"
|
||||
deletingMsg={t('Deleting send configuration ...')}
|
||||
deletedMsg={t('Send configuration deleted')}/>
|
||||
}
|
||||
|
||||
<Title>{isEdit ? t('Edit Send Configuration') : t('Create Send Configuration')}</Title>
|
||||
|
||||
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
|
||||
|
||||
<InputField id="name" label={t('Name')}/>
|
||||
<TextArea id="description" label={t('Description')} help={t('HTML is allowed')}/>
|
||||
<NamespaceSelect/>
|
||||
|
||||
<Fieldset label={t('Email Header')}>
|
||||
<InputField id="from_email" label={t('Default "from" email')}/>
|
||||
<CheckBox id="from_email_overridable" text={t('Overridable')}/>
|
||||
<InputField id="from_name" label={t('Default "from" name')}/>
|
||||
<CheckBox id="from_name_overridable" text={t('Overridable')}/>
|
||||
<InputField id="subject" label={t('Subject')}/>
|
||||
<CheckBox id="subject_overridable" text={t('Overridable')}/>
|
||||
</Fieldset>
|
||||
|
||||
<Fieldset label={t('Mailer Settings')}>
|
||||
<Dropdown id="type" label={t('Type')} options={this.typeOptions}/>
|
||||
{mailerForm}
|
||||
</Fieldset>
|
||||
|
||||
<ButtonRow>
|
||||
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
|
||||
{canDelete &&
|
||||
<NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/send-configurations/${this.props.entity.id}/delete`}/>
|
||||
}
|
||||
</ButtonRow>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
<Fieldset label={t('GPG Signing')}>
|
||||
<Trans><p>Only messages that are encrypted can be signed. Subsribers who have not set up a GPG public key in their profile receive normal email messages. Users with GPG key set receive encrypted messages and if you have signing key also set, the messages are signed with this key.</p></Trans>
|
||||
<Trans><p className="text-warning">Do not use sensitive keys here. The private key and passphrase are not encrypted in the database.</p></Trans>
|
||||
<InputField id="gpg_signing_key_passphrase" label={t('Private key passphrase')} placeholder={t('Passphrase for the key if set')} help={t('Only fill this if your private key is encrypted with a passphrase')}/>
|
||||
<TextArea id="gpg_signing_key" label={t('GPG private key')} placeholder={t('Begins with \'-----BEGIN PGP PRIVATE KEY BLOCK-----\'')} help={t('This value is optional. If you do not provide a private key GPG encrypted messages are sent without signing.')}/>
|
||||
|
||||
</Fieldset>
|
||||
|
||||
|
||||
*/
|
||||
}
|
103
client/src/send-configurations/List.js
Normal file
103
client/src/send-configurations/List.js
Normal file
|
@ -0,0 +1,103 @@
|
|||
'use strict';
|
||||
|
||||
import React, {Component} from 'react';
|
||||
import {translate} from 'react-i18next';
|
||||
import {Icon} from '../lib/bootstrap-components';
|
||||
import {
|
||||
NavButton,
|
||||
requiresAuthenticatedUser,
|
||||
Title,
|
||||
Toolbar,
|
||||
withPageHelpers
|
||||
} from '../lib/page';
|
||||
import {
|
||||
withAsyncErrorHandler,
|
||||
withErrorHandling
|
||||
} from '../lib/error-handling';
|
||||
import {Table} from '../lib/table';
|
||||
import axios from '../lib/axios';
|
||||
import moment from 'moment';
|
||||
import {getMailerTypes} from './helpers';
|
||||
|
||||
|
||||
@translate()
|
||||
@withPageHelpers
|
||||
@withErrorHandling
|
||||
@requiresAuthenticatedUser
|
||||
export default class List extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.mailerTypes = getMailerTypes(props.t);
|
||||
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async fetchPermissions() {
|
||||
const request = {
|
||||
createSendConfiguration: {
|
||||
entityTypeId: 'namespace',
|
||||
requiredOperations: ['createSendConfiguration']
|
||||
}
|
||||
};
|
||||
|
||||
const result = await axios.post('/rest/permissions-check', request);
|
||||
|
||||
this.setState({
|
||||
createPermitted: result.data.createSendConfiguration
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.fetchPermissions();
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
const columns = [
|
||||
{ data: 1, title: t('Name') },
|
||||
{ data: 2, title: t('Description') },
|
||||
{ data: 3, title: t('Type'), render: data => this.mailerTypes[data].typeName },
|
||||
{ data: 4, title: t('Created'), render: data => moment(data).fromNow() },
|
||||
{ data: 5, title: t('Namespace') },
|
||||
{
|
||||
actions: data => {
|
||||
const actions = [];
|
||||
const perms = data[6];
|
||||
|
||||
if (perms.includes('edit')) {
|
||||
actions.push({
|
||||
label: <Icon icon="edit" title={t('Edit')}/>,
|
||||
link: `/send-configurations/${data[0]}/edit`
|
||||
});
|
||||
}
|
||||
|
||||
if (perms.includes('share')) {
|
||||
actions.push({
|
||||
label: <Icon icon="share-alt" title={t('Share')}/>,
|
||||
link: `/send-configurations/${data[0]}/share`
|
||||
});
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
{this.state.createPermitted &&
|
||||
<Toolbar>
|
||||
<NavButton linkTo="/send-configurations/create" className="btn-primary" icon="plus" label={t('Create Send Configuration')}/>
|
||||
</Toolbar>
|
||||
}
|
||||
|
||||
<Title>{t('Send Configurations')}</Title>
|
||||
|
||||
<Table withHeader dataUrl="/rest/send-configurations-table" columns={columns} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
63
client/src/send-configurations/helpers.js
Normal file
63
client/src/send-configurations/helpers.js
Normal file
|
@ -0,0 +1,63 @@
|
|||
'use strict';
|
||||
|
||||
import React from "react";
|
||||
|
||||
import {MailerType} from "../../../shared/send-configurations";
|
||||
|
||||
export const mailerTypesOrder = [
|
||||
MailerType.ZONE_MTA,
|
||||
MailerType.GENERIC_SMTP,
|
||||
MailerType.AWS_SES
|
||||
];
|
||||
|
||||
export function getMailerTypes(t) {
|
||||
const mailerTypes = {};
|
||||
|
||||
function clearBeforeSend(data) {
|
||||
}
|
||||
|
||||
mailerTypes[MailerType.GENERIC_SMTP] = {
|
||||
typeName: t('Generic SMTP'),
|
||||
getForm: owner => null,
|
||||
initData: () => ({
|
||||
}),
|
||||
afterLoad: data => {
|
||||
},
|
||||
beforeSave: data => {
|
||||
clearBeforeSend(data);
|
||||
},
|
||||
afterTypeChange: mutState => {
|
||||
// mutState.setIn(['type', 'value'], '');
|
||||
}
|
||||
};
|
||||
|
||||
mailerTypes[MailerType.ZONE_MTA] = {
|
||||
typeName: t('Zone MTA'),
|
||||
getForm: owner => null,
|
||||
initData: () => ({
|
||||
}),
|
||||
afterLoad: data => {
|
||||
},
|
||||
beforeSave: data => {
|
||||
clearBeforeSend(data);
|
||||
},
|
||||
afterTypeChange: mutState => {
|
||||
}
|
||||
};
|
||||
|
||||
mailerTypes[MailerType.AWS_SES] = {
|
||||
typeName: t('Amazon SES'),
|
||||
getForm: owner => null,
|
||||
initData: () => ({
|
||||
}),
|
||||
afterLoad: data => {
|
||||
},
|
||||
beforeSave: data => {
|
||||
clearBeforeSend(data);
|
||||
},
|
||||
afterTypeChange: mutState => {
|
||||
}
|
||||
};
|
||||
|
||||
return mailerTypes;
|
||||
}
|
49
client/src/send-configurations/root.js
Normal file
49
client/src/send-configurations/root.js
Normal file
|
@ -0,0 +1,49 @@
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import CUD from './CUD';
|
||||
import List from './List';
|
||||
import Share from '../shares/Share';
|
||||
|
||||
|
||||
function getMenus(t) {
|
||||
return {
|
||||
'send-configurations': {
|
||||
title: t('Send Configurations'),
|
||||
link: '/send-configurations',
|
||||
panelComponent: List,
|
||||
children: {
|
||||
':sendConfigurationId([0-9]+)': {
|
||||
title: resolved => t('Template "{{name}}"', {name: resolved.sendConfiguration.name}),
|
||||
resolve: {
|
||||
sendConfiguration: params => `/rest/send-configurations/${params.sendConfigurationId}`
|
||||
},
|
||||
link: params => `/send-configurations/${params.sendConfigurationId}/edit`,
|
||||
navs: {
|
||||
':action(edit|delete)': {
|
||||
title: t('Edit'),
|
||||
link: params => `/send-configurations/${params.sendConfigurationId}/edit`,
|
||||
visible: resolved => resolved.sendConfiguration.permissions.includes('edit'),
|
||||
panelRender: props => <CUD action={props.match.params.action} entity={props.resolved.sendConfiguration} />
|
||||
},
|
||||
share: {
|
||||
title: t('Share'),
|
||||
link: params => `/send-configurations/${params.sendConfigurationId}/share`,
|
||||
visible: resolved => resolved.sendConfiguration.permissions.includes('share'),
|
||||
panelRender: props => <Share title={t('Share')} entity={props.resolved.sendConfiguration} entityTypeId="sendConfiguration" />
|
||||
}
|
||||
}
|
||||
},
|
||||
create: {
|
||||
title: t('Create'),
|
||||
panelRender: props => <CUD action="create" />
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
getMenus
|
||||
}
|
|
@ -10,7 +10,7 @@ import { validateNamespace, NamespaceSelect } from '../../lib/namespace';
|
|||
import {DeleteModalDialog} from "../../lib/modals";
|
||||
|
||||
import { versafix } from "../../../../shared/mosaico-templates";
|
||||
import { getTemplateTypes } from "./helpers";
|
||||
import { getTemplateTypes, getTemplateTypesOrder } from "./helpers";
|
||||
|
||||
@translate()
|
||||
@withForm
|
||||
|
@ -23,6 +23,14 @@ export default class CUD extends Component {
|
|||
|
||||
this.templateTypes = getTemplateTypes(props.t);
|
||||
|
||||
this.typeOptions = [];
|
||||
for (const type of getTemplateTypesOrder()) {
|
||||
this.typeOptions.push({
|
||||
key: type,
|
||||
label: this.templateTypes[type].typeName
|
||||
});
|
||||
}
|
||||
|
||||
this.state = {};
|
||||
|
||||
this.initForm();
|
||||
|
@ -141,14 +149,6 @@ export default class CUD extends Component {
|
|||
form = this.templateTypes[typeKey].getForm(this);
|
||||
}
|
||||
|
||||
const typeOptions = [];
|
||||
for (const type of ['html', 'mjml']) {
|
||||
typeOptions.push({
|
||||
key: type,
|
||||
label: this.templateTypes.typeName
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{canDelete &&
|
||||
|
@ -158,7 +158,7 @@ export default class CUD extends Component {
|
|||
deleteUrl={`/rest/templates/mosaico/${this.props.entity.id}`}
|
||||
cudUrl={`/templates/mosaico/${this.props.entity.id}/edit`}
|
||||
listUrl="/templates/mosaico"
|
||||
deletingMsg={t('Deleting mosaico template ...')}
|
||||
deletingMsg={t('Deleting Mosaico template ...')}
|
||||
deletedMsg={t('Mosaico template deleted')}/>
|
||||
}
|
||||
|
||||
|
@ -167,7 +167,7 @@ export default class CUD extends Component {
|
|||
<Form stateOwner={this} onSubmitAsync={::this.submitAndLeave}>
|
||||
<InputField id="name" label={t('Name')}/>
|
||||
<TextArea id="description" label={t('Description')} help={t('HTML is allowed')}/>
|
||||
<Dropdown id="type" label={t('Type')} options={typeOptions}/>
|
||||
<Dropdown id="type" label={t('Type')} options={this.typeOptions}/>
|
||||
<NamespaceSelect/>
|
||||
|
||||
{form}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import React, { Component } from 'react';
|
||||
import { translate } from 'react-i18next';
|
||||
import {DropdownMenu, Icon} from '../../lib/bootstrap-components';
|
||||
import { requiresAuthenticatedUser, withPageHelpers, Title, Toolbar, DropdownLink } from '../../lib/page';
|
||||
import { requiresAuthenticatedUser, withPageHelpers, Title, Toolbar, MenuLink } from '../../lib/page';
|
||||
import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handling';
|
||||
import { Table } from '../../lib/table';
|
||||
import axios from '../../lib/axios';
|
||||
|
@ -82,8 +82,8 @@ export default class List extends Component {
|
|||
{this.state.createPermitted &&
|
||||
<Toolbar>
|
||||
<DropdownMenu className="btn-primary" label={t('Create Mosaico Template')}>
|
||||
<DropdownLink to="/templates/mosaico/create">{t('Blank')}</DropdownLink>
|
||||
<DropdownLink to="/templates/mosaico/create/versafix">{t('Versafix One')}</DropdownLink>
|
||||
<MenuLink to="/templates/mosaico/create">{t('Blank')}</MenuLink>
|
||||
<MenuLink to="/templates/mosaico/create/versafix">{t('Versafix One')}</MenuLink>
|
||||
</DropdownMenu>
|
||||
</Toolbar>
|
||||
}
|
||||
|
|
|
@ -5,6 +5,10 @@ import {ACEEditor} from "../../lib/form";
|
|||
import 'brace/mode/html'
|
||||
import 'brace/mode/xml'
|
||||
|
||||
export function getTemplateTypesOrder() {
|
||||
return ['mjml', 'html'];
|
||||
}
|
||||
|
||||
export function getTemplateTypes(t) {
|
||||
const templateTypes = {};
|
||||
|
||||
|
@ -17,7 +21,6 @@ export function getTemplateTypes(t) {
|
|||
typeName: t('HTML'),
|
||||
getForm: owner => <ACEEditor id="html" height="700px" mode="html" label={t('Template content')}/>,
|
||||
afterLoad: data => {
|
||||
console.log(data);
|
||||
data.html = data.data.html;
|
||||
},
|
||||
beforeSave: (data) => {
|
||||
|
|
|
@ -198,15 +198,16 @@ browser="phantomjs"
|
|||
name="Master"
|
||||
admin=true
|
||||
description="All permissions"
|
||||
permissions=["rebuildPermissions", "createJavascriptWithROAccess", "manageBlacklist"]
|
||||
permissions=["rebuildPermissions", "createJavascriptWithROAccess", "manageBlacklist", "manageSettings"]
|
||||
rootNamespaceRole="master"
|
||||
|
||||
[roles.namespace.master]
|
||||
name="Master"
|
||||
description="All permissions"
|
||||
permissions=["view", "edit", "delete", "share", "createNamespace", "createList", "createCustomForm", "createReport", "createReportTemplate", "createTemplate", "createMosaicoTemplate", "manageUsers"]
|
||||
permissions=["view", "edit", "delete", "share", "createNamespace", "createList", "createCustomForm", "createReport", "createReportTemplate", "createTemplate", "createMosaicoTemplate", "createSendConfiguration", "manageUsers"]
|
||||
|
||||
[roles.namespace.master.children]
|
||||
sendConfiguration=["view", "edit", "delete", "share", "send", "overrideAllowed", "overrideAll"]
|
||||
list=["view", "edit", "delete", "share", "manageFields", "viewSubscriptions", "manageSubscriptions", "manageSegments"]
|
||||
customForm=["view", "edit", "delete", "share"]
|
||||
campaign=["view", "edit", "delete", "share", "manageFiles"]
|
||||
|
@ -214,7 +215,12 @@ template=["view", "edit", "delete", "share", "manageFiles"]
|
|||
report=["view", "edit", "delete", "share", "execute", "viewContent", "viewOutput"]
|
||||
reportTemplate=["view", "edit", "delete", "share", "execute"]
|
||||
mosaicoTemplate=["view", "edit", "delete", "share", "manageFiles"]
|
||||
namespace=["view", "edit", "delete", "share", "createNamespace", "createList", "createCustomForm", "createReport", "createReportTemplate", "createTemplate", "createMosaicoTemplate", "manageUsers"]
|
||||
namespace=["view", "edit", "delete", "share", "createNamespace", "createList", "createCustomForm", "createReport", "createReportTemplate", "createTemplate", "createMosaicoTemplate", "createSendConfiguration", "manageUsers"]
|
||||
|
||||
[roles.sendConfiguration.master]
|
||||
name="Master"
|
||||
description="All permissions"
|
||||
permissions=["view", "edit", "delete", "share", "send", "overrideAllowed", "overrideAll"]
|
||||
|
||||
[roles.list.master]
|
||||
name="Master"
|
||||
|
@ -264,6 +270,7 @@ description="XXX"
|
|||
permissions=["view", "edit", "delete"]
|
||||
|
||||
[roles.namespace.editor.children]
|
||||
sendConfiguration=[]
|
||||
list=[]
|
||||
customForm=[]
|
||||
campaign=[]
|
||||
|
@ -271,6 +278,7 @@ template=[]
|
|||
report=[]
|
||||
reportTemplate=[]
|
||||
namespace=["view", "edit", "delete"]
|
||||
mosaicoTemplate=[]
|
||||
|
||||
[roles.list.editor]
|
||||
name="Editor"
|
||||
|
@ -304,5 +312,10 @@ permissions=[]
|
|||
|
||||
[roles.mosaicoTemplate.editor]
|
||||
name="Editor"
|
||||
description="All permissions"
|
||||
description="XXX"
|
||||
permissions=[]
|
||||
|
||||
[roles.sendConfiguration.editor]
|
||||
name="Editor"
|
||||
description="XXX"
|
||||
permissions=[]
|
||||
|
|
|
@ -28,6 +28,11 @@ const entityTypes = {
|
|||
permissionsTable: 'permissions_template',
|
||||
filesTable: 'files_template'
|
||||
},
|
||||
sendConfiguration: {
|
||||
entitiesTable: 'send_configurations',
|
||||
sharesTable: 'shares_send_configuration',
|
||||
permissionsTable: 'permissions_send_configuration'
|
||||
},
|
||||
report: {
|
||||
entitiesTable: 'reports',
|
||||
sharesTable: 'shares_report',
|
||||
|
|
|
@ -36,6 +36,7 @@ async function listDTAjax(context, params) {
|
|||
|
||||
async function _validateAndPreprocess(tx, entity) {
|
||||
entity.data = JSON.stringify(entity.data);
|
||||
await namespaceHelpers.validateEntity(tx, entity);
|
||||
}
|
||||
|
||||
async function create(context, entity) {
|
||||
|
@ -44,8 +45,6 @@ async function create(context, entity) {
|
|||
|
||||
await _validateAndPreprocess(tx, entity);
|
||||
|
||||
await namespaceHelpers.validateEntity(tx, entity);
|
||||
|
||||
const ids = await tx('mosaico_templates').insert(filterObject(entity, allowedKeys));
|
||||
const id = ids[0];
|
||||
|
||||
|
@ -74,7 +73,6 @@ async function updateWithConsistencyCheck(context, entity) {
|
|||
|
||||
await _validateAndPreprocess(tx, entity);
|
||||
|
||||
await namespaceHelpers.validateEntity(tx, entity);
|
||||
await namespaceHelpers.validateMove(context, entity, existing, 'mosaicoTemplate', 'createMosaicoTemplate', 'delete');
|
||||
|
||||
await tx('mosaico_templates').where('id', entity.id).update(filterObject(entity, allowedKeys));
|
||||
|
|
114
models/send-configuration.js
Normal file
114
models/send-configuration.js
Normal file
|
@ -0,0 +1,114 @@
|
|||
'use strict';
|
||||
|
||||
const knex = require('../lib/knex');
|
||||
const hasher = require('node-object-hash')();
|
||||
const dtHelpers = require('../lib/dt-helpers');
|
||||
const { enforce, filterObject } = require('../lib/helpers');
|
||||
const interoperableErrors = require('../shared/interoperable-errors');
|
||||
const shares = require('./shares');
|
||||
const namespaceHelpers = require('../lib/namespace-helpers');
|
||||
const {MailerType} = require('../shared/send-configurations');
|
||||
|
||||
const allowedKeys = new Set(['name', 'description', 'from_email', 'from_email_overridable', 'from_name', 'from_name_overridable', 'subject', 'subject_overridable', 'mailer_type', 'mailer_settings', 'namespace']);
|
||||
|
||||
|
||||
function hash(entity) {
|
||||
return hasher.hash(filterObject(entity, allowedKeys));
|
||||
}
|
||||
|
||||
|
||||
async function listDTAjax(context, params) {
|
||||
return await dtHelpers.ajaxListWithPermissions(
|
||||
context,
|
||||
[{ entityTypeId: 'sendConfiguration', requiredOperations: ['view'] }],
|
||||
params,
|
||||
builder => builder
|
||||
.from('send_configurations')
|
||||
.innerJoin('namespaces', 'namespaces.id', 'send_configurations.namespace'),
|
||||
['send_configurations.id', 'send_configurations.name', 'send_configurations.description', 'send_configurations.mailer_type', 'send_configurations.created', 'namespaces.name']
|
||||
);
|
||||
}
|
||||
|
||||
async function getById(context, id, withPermissions = true) {
|
||||
return await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'sendConfiguration', id, 'view');
|
||||
const entity = await tx('send_configurations').where('id', id).first();
|
||||
entity.mailer_settings = JSON.parse(entity.mailer_settings);
|
||||
|
||||
// note that permissions are optional as as this methods may be used with synthetic admin context
|
||||
if (withPermissions) {
|
||||
entity.permissions = await shares.getPermissionsTx(tx, context, 'sendConfiguration', id);
|
||||
}
|
||||
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
async function _validateAndPreprocess(tx, entity, isCreate) {
|
||||
await namespaceHelpers.validateEntity(tx, entity);
|
||||
|
||||
enforce(entity.mailer_type >= 0 && entity.mailer_type < MailerType.MAX, 'Unknown mailer type');
|
||||
entity.mailer_settings = JSON.stringify(entity.mailer_settings);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function create(context, entity) {
|
||||
return await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'namespace', entity.namespace, 'createSendConfiguration');
|
||||
|
||||
await _validateAndPreprocess(tx, entity);
|
||||
|
||||
const ids = await tx('send_configurations').insert(filterObject(entity, allowedKeys));
|
||||
const id = ids[0];
|
||||
|
||||
await shares.rebuildPermissionsTx(tx, { entityTypeId: 'sendConfiguration', entityId: id });
|
||||
|
||||
return id;
|
||||
});
|
||||
}
|
||||
|
||||
async function updateWithConsistencyCheck(context, entity) {
|
||||
await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'list', entity.id, 'edit');
|
||||
|
||||
const existing = await tx('send_configurations').where('id', entity.id).first();
|
||||
if (!existing) {
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
}
|
||||
|
||||
existing.mailer_settings = JSON.parse(existing.mailer_settings);
|
||||
|
||||
const existingHash = hash(existing);
|
||||
if (existingHash !== entity.originalHash) {
|
||||
throw new interoperableErrors.ChangedError();
|
||||
}
|
||||
|
||||
await _validateAndPreprocess(tx, entity);
|
||||
|
||||
await namespaceHelpers.validateMove(context, entity, existing, 'sendConfiguration', 'createSendConfiguration', 'delete');
|
||||
|
||||
await tx('send_configurations').where('id', entity.id).update(filterObject(entity, allowedKeys));
|
||||
|
||||
await shares.rebuildPermissionsTx(tx, { entityTypeId: 'sendConfiguration', entityId: entity.id });
|
||||
});
|
||||
}
|
||||
|
||||
async function remove(context, id) {
|
||||
await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'sendConfiguration', id, 'delete');
|
||||
|
||||
await tx('send_configurations').where('id', id).del();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
MailerType,
|
||||
hash,
|
||||
listDTAjax,
|
||||
getById,
|
||||
create,
|
||||
updateWithConsistencyCheck,
|
||||
remove
|
||||
};
|
|
@ -1,11 +1,18 @@
|
|||
'use strict';
|
||||
|
||||
const knex = require('../lib/knex');
|
||||
const tools = require('../lib/tools');
|
||||
const shares = require('./shares');
|
||||
|
||||
const allowedKeys = new Set(['adminEmail', 'uaCode', 'pgpPassphrase', 'pgpPrivateKey', 'defaultHomepage']);
|
||||
// defaultHomepage is used as a default to list.homepage - if the list.homepage is not filled in
|
||||
|
||||
async function get(context, keyOrKeys) {
|
||||
shares.enforceGlobalPermission(context, 'manageSettings');
|
||||
|
||||
async function get(keyOrKeys) {
|
||||
let keys;
|
||||
if (!Array.isArray(keyOrKeys)) {
|
||||
if (!keyOrKeys) {
|
||||
keys = allowedKeys.values();
|
||||
} else if (!Array.isArray(keyOrKeys)) {
|
||||
keys = [ keys ];
|
||||
} else {
|
||||
keys = keyOrKeys;
|
||||
|
@ -25,11 +32,18 @@ async function get(keyOrKeys) {
|
|||
}
|
||||
}
|
||||
|
||||
async function set(key, value) {
|
||||
try {
|
||||
await knex('settings').insert({key, value});
|
||||
} catch (err) {
|
||||
await knex('settings').where('key', key).update('value', value);
|
||||
async function set(context, data) {
|
||||
shares.enforceGlobalPermission(context, 'manageSettings');
|
||||
|
||||
for (const key in data) {
|
||||
if (allowedKeys.has(key)) {
|
||||
const value = data[key];
|
||||
try {
|
||||
await knex('settings').insert({key, value});
|
||||
} catch (err) {
|
||||
await knex('settings').where('key', key).update('value', value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -371,12 +371,12 @@ function registerRestrictedAccessTokenMethod(method, getHandlerFromParams) {
|
|||
restrictedAccessTokenMethods[method] = getHandlerFromParams;
|
||||
}
|
||||
|
||||
function getRestrictedAccessToken(context, method, params) {
|
||||
async function getRestrictedAccessToken(context, method, params) {
|
||||
const token = crypto.randomBytes(24).toString('hex').toLowerCase();
|
||||
const tokenEntry = {
|
||||
token,
|
||||
userId: context.user.id,
|
||||
handler: restrictedAccessTokenMethods[method](params),
|
||||
handler: await restrictedAccessTokenMethods[method](params),
|
||||
expires: Date.now() + 120 * 1000
|
||||
};
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ const files = require('../models/files');
|
|||
const fileHelpers = require('../lib/file-helpers');
|
||||
|
||||
|
||||
users.registerRestrictedAccessTokenMethod('mosaico', ({entityTypeId, entityId}) => {
|
||||
users.registerRestrictedAccessTokenMethod('mosaico', async ({entityTypeId, entityId}) => {
|
||||
if (entityTypeId === 'template' || entityTypeId === 'campaign') {
|
||||
return {
|
||||
permissions: {
|
||||
|
|
36
routes/rest/send-configurations.js
Normal file
36
routes/rest/send-configurations.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
'use strict';
|
||||
|
||||
const passport = require('../../lib/passport');
|
||||
const sendConfigurations = require('../../models/send-configuration');
|
||||
|
||||
const router = require('../../lib/router-async').create();
|
||||
|
||||
|
||||
router.getAsync('/send-configurations/:sendConfigurationId', passport.loggedIn, async (req, res) => {
|
||||
const sendConfiguration = await sendConfigurations.getById(req.context, req.params.sendConfigurationId);
|
||||
sendConfiguration.hash = sendConfigurations.hash(sendConfiguration);
|
||||
return res.json(sendConfiguration);
|
||||
});
|
||||
|
||||
router.postAsync('/send-configurations', passport.loggedIn, passport.csrfProtection, async (req, res) => {
|
||||
return res.json(await sendConfigurations.create(req.context, req.body));
|
||||
});
|
||||
|
||||
router.putAsync('/send-configurations/:sendConfigurationId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
|
||||
const sendConfiguration = req.body;
|
||||
sendConfiguration.id = parseInt(req.params.sendConfigurationId);
|
||||
|
||||
await sendConfigurations.updateWithConsistencyCheck(req.context, sendConfiguration);
|
||||
return res.json();
|
||||
});
|
||||
|
||||
router.deleteAsync('/send-configurations/:sendConfigurationId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
|
||||
await sendConfigurations.remove(req.context, req.params.sendConfigurationId);
|
||||
return res.json();
|
||||
});
|
||||
|
||||
router.postAsync('/send-configurations-table', passport.loggedIn, async (req, res) => {
|
||||
return res.json(await sendConfigurations.listDTAjax(req.context, req.body));
|
||||
});
|
||||
|
||||
module.exports = router;
|
|
@ -0,0 +1,41 @@
|
|||
exports.up = (knex, Promise) => (async() => {
|
||||
await knex.schema.createTable('send_configurations', table => {
|
||||
table.increments('id').primary();
|
||||
table.string('name');
|
||||
table.text('description');
|
||||
table.string('from_email');
|
||||
table.boolean('from_email_overridable').defaultTo(false);
|
||||
table.string('from_name');
|
||||
table.boolean('from_name_overridable').defaultTo(false);
|
||||
table.string('subject');
|
||||
table.boolean('subject_overridable').defaultTo(false);
|
||||
table.string('mailer_type');
|
||||
table.text('mailer_settings', 'longtext');
|
||||
table.timestamp('created').defaultTo(knex.fn.now());
|
||||
table.integer('namespace').unsigned().references('namespaces.id');
|
||||
});
|
||||
|
||||
await knex.schema.createTable(`shares_send_configuration`, table => {
|
||||
table.integer('entity').unsigned().notNullable().references(`send_configurations.id`).onDelete('CASCADE');
|
||||
table.integer('user').unsigned().notNullable().references('users.id').onDelete('CASCADE');
|
||||
table.string('role', 128).notNullable();
|
||||
table.boolean('auto').defaultTo(false);
|
||||
table.primary(['entity', 'user']);
|
||||
});
|
||||
|
||||
await knex.schema.createTable(`permissions_send_configuration`, table => {
|
||||
table.integer('entity').unsigned().notNullable().references(`send_configurations.id`).onDelete('CASCADE');
|
||||
table.integer('user').unsigned().notNullable().references('users.id').onDelete('CASCADE');
|
||||
table.string('operation', 128).notNullable();
|
||||
table.primary(['entity', 'user', 'operation']);
|
||||
});
|
||||
|
||||
})();
|
||||
|
||||
exports.down = (knex, Promise) => (async() => {
|
||||
await knex.schema
|
||||
.dropTable('shares_send_configuration')
|
||||
.dropTable('permissions_send_configuration')
|
||||
.dropTable('send_configurations')
|
||||
;
|
||||
})();
|
|
@ -0,0 +1,5 @@
|
|||
exports.up = (knex, Promise) => (async() => {
|
||||
})();
|
||||
|
||||
exports.down = (knex, Promise) => (async() => {
|
||||
})();
|
12
shared/send-configurations.js
Normal file
12
shared/send-configurations.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
'use strict';
|
||||
|
||||
const MailerType = {
|
||||
GENERIC_SMTP: 0,
|
||||
ZONE_MTA: 1,
|
||||
AWS_SES: 2,
|
||||
MAX: 3
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
MailerType
|
||||
};
|
|
@ -45,7 +45,7 @@
|
|||
<mj-section padding-top="0">
|
||||
<mj-column>
|
||||
<mj-text mj-class="small" font-style="italic">
|
||||
<!-- {{defaultPostaddress}} -->
|
||||
<!-- POST ADDRESS -->
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
|
Loading…
Reference in a new issue