Files can be added to templates and managed in a dedicated "Files" view.
Mosaico integration in progress. The files seem to be working for Mosaico.
This commit is contained in:
parent
c85f2d4440
commit
b5cdf57f72
23 changed files with 506 additions and 164 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -31,3 +31,5 @@ public/grapejs/templates/*
|
|||
config/production.toml
|
||||
workers/reports/config/production.toml
|
||||
docker-compose.override.yml
|
||||
|
||||
/files
|
||||
|
|
15
app.js
15
app.js
|
@ -30,6 +30,7 @@ const api = require('./routes/api');
|
|||
const reports = require('./routes/reports');
|
||||
const subscription = require('./routes/subscription');
|
||||
const mosaico = require('./routes/mosaico');
|
||||
const files = require('./routes/files');
|
||||
|
||||
const namespacesRest = require('./routes/rest/namespaces');
|
||||
const usersRest = require('./routes/rest/users');
|
||||
|
@ -46,6 +47,7 @@ const subscriptionsRest = require('./routes/rest/subscriptions');
|
|||
const templatesRest = require('./routes/rest/templates');
|
||||
const blacklistRest = require('./routes/rest/blacklist');
|
||||
const editorsRest = require('./routes/rest/editors');
|
||||
const filesRest = require('./routes/rest/files');
|
||||
|
||||
const root = require('./routes/root');
|
||||
|
||||
|
@ -55,15 +57,11 @@ const app = express();
|
|||
|
||||
function install404Fallback(url) {
|
||||
app.use(url, (req, res, next) => {
|
||||
let err = new Error(_('Not Found'));
|
||||
err.status = 404;
|
||||
next(err);
|
||||
next(new interoperableErrors.NotFoundError());
|
||||
});
|
||||
|
||||
app.use(url + '/*', (req, res, next) => {
|
||||
let err = new Error(_('Not Found'));
|
||||
err.status = 404;
|
||||
next(err);
|
||||
next(new interoperableErrors.NotFoundError());
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -254,17 +252,17 @@ app.use((req, res, next) => {
|
|||
|
||||
// Regular endpoints
|
||||
useWith404Fallback('/subscription', subscription);
|
||||
useWith404Fallback('/files', files);
|
||||
useWith404Fallback('/mosaico', mosaico);
|
||||
|
||||
if (config.reports && config.reports.enabled === true) {
|
||||
useWith404Fallback('/reports', reports);
|
||||
}
|
||||
|
||||
useWith404Fallback('/mosaico', mosaico);
|
||||
|
||||
// API endpoints
|
||||
useWith404Fallback('/api', api);
|
||||
|
||||
/* ------------------------------------------------------------------- */
|
||||
|
||||
// REST endpoints
|
||||
app.use('/rest', namespacesRest);
|
||||
|
@ -280,6 +278,7 @@ app.use('/rest', subscriptionsRest);
|
|||
app.use('/rest', templatesRest);
|
||||
app.use('/rest', blacklistRest);
|
||||
app.use('/rest', editorsRest);
|
||||
app.use('/rest', filesRest);
|
||||
|
||||
if (config.reports && config.reports.enabled === true) {
|
||||
app.use('/rest', reportTemplatesRest);
|
||||
|
|
|
@ -3,18 +3,19 @@
|
|||
import React, {Component} from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import {translate} from "react-i18next";
|
||||
import {requiresAuthenticatedUser} from "./lib/page";
|
||||
import {ACEEditor, Button, Form, FormSendMethod, withForm} from "./lib/form";
|
||||
import {withErrorHandling} from "./lib/error-handling";
|
||||
import {Table} from "./lib/table";
|
||||
import {requiresAuthenticatedUser} from "./page";
|
||||
import {withErrorHandling} from "./error-handling";
|
||||
import {Table} from "./table";
|
||||
import Dropzone from "react-dropzone";
|
||||
import {ModalDialog} from "./lib/modals";
|
||||
import {Icon} from "./lib/bootstrap-components";
|
||||
import {ModalDialog} from "./modals";
|
||||
import {Icon} from "./bootstrap-components";
|
||||
import axios from './axios';
|
||||
import styles from "./styles.scss";
|
||||
import {withPageHelpers} from "./page";
|
||||
|
||||
@translate()
|
||||
@withForm
|
||||
@withErrorHandling
|
||||
@withPageHelpers
|
||||
@requiresAuthenticatedUser
|
||||
export default class Files extends Component {
|
||||
constructor(props) {
|
||||
|
@ -26,16 +27,18 @@ export default class Files extends Component {
|
|||
};
|
||||
|
||||
const t = props.t;
|
||||
|
||||
this.initForm();
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
title: PropTypes.string,
|
||||
entity: PropTypes.object,
|
||||
entityTypeId: PropTypes.string
|
||||
entityTypeId: PropTypes.string,
|
||||
usePublicDownloadUrls: PropTypes.bool
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
usePublicDownloadUrls: true
|
||||
}
|
||||
|
||||
getFilesUploadedMessage(response){
|
||||
const t = this.props.t;
|
||||
|
@ -56,21 +59,21 @@ export default class Files extends Component {
|
|||
onDrop(files){
|
||||
const t = this.props.t;
|
||||
if (files.length > 0) {
|
||||
this.setFormStatusMessage('info', t('Uploading {{count}} file(s)', files.length));
|
||||
this.setFlashMessage('info', t('Uploading {{count}} file(s)', files.length));
|
||||
const data = new FormData();
|
||||
for (const file of files) {
|
||||
data.append('file', file)
|
||||
data.append('files[]', file)
|
||||
}
|
||||
axios.put(`/rest/files/${this.props.entityTypeId}, ${this.props.entity.id}`, data)
|
||||
axios.post(`/rest/files/${this.props.entityTypeId}/${this.props.entity.id}`, data)
|
||||
.then(res => {
|
||||
this.filesTable.refresh();
|
||||
const message = this.getFilesUploadedMessage(res);
|
||||
this.setFormStatusMessage('info', message);
|
||||
this.setFlashMessage('info', message);
|
||||
})
|
||||
.catch(res => this.setFormStatusMessage('danger', t('File upload failed: ') + res.message));
|
||||
.catch(res => this.setFlashMessage('danger', t('File upload failed: ') + res.message));
|
||||
}
|
||||
else{
|
||||
this.setFormStatusMessage('info', t('No files to upload'));
|
||||
this.setFlashMessage('info', t('No files to upload'));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -88,16 +91,13 @@ export default class Files extends Component {
|
|||
await this.hideDeleteFile();
|
||||
|
||||
try {
|
||||
this.disableForm();
|
||||
this.setFormStatusMessage('info', t('Deleting file ...'));
|
||||
this.setFlashMessage('info', t('Deleting file ...'));
|
||||
await axios.delete(`/rest/files/${this.props.entityTypeId}/${fileToDeleteId}`);
|
||||
this.filesTable.refresh();
|
||||
this.setFormStatusMessage('info', t('File deleted'));
|
||||
this.enableForm();
|
||||
this.setFlashMessage('info', t('File deleted'));
|
||||
} catch (err) {
|
||||
this.filesTable.refresh();
|
||||
this.setFormStatusMessage('danger', t('Delete file failed: ') + err.message);
|
||||
this.enableForm();
|
||||
this.setFlashMessage('danger', t('Delete file failed: ') + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -106,14 +106,21 @@ export default class Files extends Component {
|
|||
|
||||
const columns = [
|
||||
{ data: 1, title: "Name" },
|
||||
{ data: 2, title: "Size" },
|
||||
{ data: 3, title: "Size" },
|
||||
{
|
||||
actions: data => {
|
||||
|
||||
let downloadUrl;
|
||||
if (this.props.usePublicDownloadUrls) {
|
||||
downloadUrl =`/files/${this.props.entityTypeId}/${this.props.entity.id}/${data[2]}`;
|
||||
} else {
|
||||
downloadUrl =`/rest/files/${this.props.entityTypeId}/${data[0]}`;
|
||||
}
|
||||
|
||||
const actions = [
|
||||
{
|
||||
label: <Icon icon="download" title={t('Download')}/>,
|
||||
href: `/rest/files/${this.props.entityTypeId}/${data[0]}`
|
||||
href: downloadUrl
|
||||
},
|
||||
{
|
||||
label: <Icon icon="remove" title={t('Delete')}/>,
|
||||
|
@ -138,10 +145,10 @@ export default class Files extends Component {
|
|||
]}>
|
||||
{t('Are you sure you want to delete file "{{name}}"?', {name: this.state.fileToDeleteName})}
|
||||
</ModalDialog>
|
||||
<Dropzone onDrop={::this.onDrop} className="dropZone" activeClassName="dropZoneActive">
|
||||
<Dropzone onDrop={::this.onDrop} className={styles.dropZone} activeClassName="dropZoneActive">
|
||||
{state => state.isDragActive ? t('Drop {{count}} file(s)', {count:state.draggedFiles.length}) : t('Drop files here')}
|
||||
</Dropzone>
|
||||
<Table withHeader ref={node => this.filesTable = node} dataUrl={`/rest/template-files-table/${this.props.entity.id}`} columns={columns} />
|
||||
<Table withHeader ref={node => this.filesTable = node} dataUrl={`/rest/files-table/${this.props.entityTypeId}/${this.props.entity.id}`} columns={columns} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -87,3 +87,16 @@
|
|||
.secondaryNav > li > a {
|
||||
padding: 3px 10px;
|
||||
}
|
||||
|
||||
.dropZone {
|
||||
padding-top: 30px;
|
||||
padding-bottom: 30px;
|
||||
margin-bottom: 15px;
|
||||
margin-top: 8px;
|
||||
border: 2px solid #E6E9ED;
|
||||
border-radius: 5px;
|
||||
background-color: #FAFAD2;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
color: #808080;
|
||||
}
|
||||
|
|
|
@ -8,20 +8,41 @@ import {
|
|||
} from 'react-i18next';
|
||||
import i18n from '../lib/i18n';
|
||||
import PropTypes from "prop-types";
|
||||
import styles from "./styles.scss";
|
||||
|
||||
const ResourceType = {
|
||||
TEMPLATE: 'template',
|
||||
CAMPAIGN: 'campaign'
|
||||
}
|
||||
|
||||
@translate()
|
||||
class MosaicoEditor extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.viewModel = null;
|
||||
this.state = {
|
||||
entityTypeId: ResourceType.TEMPLATE, // FIXME
|
||||
entityId: 13 // FIXME
|
||||
}
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
//structure: PropTypes.oneOfType([PropTypes.object, PropTypes.func]).isRequired,
|
||||
}
|
||||
|
||||
async onClose(evt) {
|
||||
const t = this.props.t;
|
||||
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
|
||||
if (confirm(t('Unsaved changes will be lost. Close now?'))) {
|
||||
window.location.href = `/${this.state.entityTypeId}s/${this.state.entityId}/edit`;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const basePath = '/public/mosaico';
|
||||
const publicPath = '/public/mosaico';
|
||||
|
||||
if (!Mosaico.isCompatible()) {
|
||||
alert('Update your browser!');
|
||||
|
@ -30,35 +51,57 @@ class MosaicoEditor extends Component {
|
|||
|
||||
const plugins = window.mosaicoPlugins;
|
||||
|
||||
plugins.push(viewModel => {
|
||||
this.viewModel = viewModel;
|
||||
});
|
||||
|
||||
// (Custom) HTML postRenderers
|
||||
plugins.push(viewModel => {
|
||||
viewModel.originalExportHTML = viewModel.exportHTML;
|
||||
viewModel.exportHTML = () => {
|
||||
let html = viewModel.originalExportHTML();
|
||||
for (const portRender of window.mosaicoHTMLPostRenderers) {
|
||||
html = postRender(html);
|
||||
}
|
||||
return html;
|
||||
};
|
||||
});
|
||||
|
||||
plugins.unshift(vm => {
|
||||
// This is a fix for the use of hardcoded path in Mosaico
|
||||
vm.logoPath = basePath + '/img/mosaico32.png'
|
||||
vm.logoPath = publicPath + '/img/mosaico32.png'
|
||||
});
|
||||
|
||||
const config = {
|
||||
imgProcessorBackend: basePath+'/img/',
|
||||
emailProcessorBackend: basePath+'/dl/',
|
||||
imgProcessorBackend: `/mosaico/img/${this.state.entityTypeId}/${this.state.entityId}`,
|
||||
emailProcessorBackend: '/mosaico/dl/',
|
||||
titleToken: "MOSAICO Responsive Email Designer",
|
||||
fileuploadConfig: {
|
||||
url: basePath+'/upload/'
|
||||
url: `/mosaico/upload/${this.state.entityTypeId}/${this.state.entityId}`
|
||||
},
|
||||
strings: window.mosaicoLanguageStrings
|
||||
};
|
||||
|
||||
const metadata = undefined;
|
||||
const model = undefined;
|
||||
const template = basePath + '/templates/versafix-1/index.html';
|
||||
const template = publicPath + '/templates/versafix-1/index.html';
|
||||
|
||||
Mosaico.start(config, template, metadata, model, plugins);
|
||||
const allPlugins = plugins.concat(window.mosaicoPlugins);
|
||||
|
||||
Mosaico.start(config, template, metadata, model, allPlugins);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.navbar}>
|
||||
<img className={styles.logo} src="/public/mailtrain-header.png"/>
|
||||
<a className={styles.btn} onClick={::this.onClose}>{t('CLOSE')}</a>
|
||||
<a className={styles.btn}><span></span></a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
35
client/src/mosaico/styles.scss
Normal file
35
client/src/mosaico/styles.scss
Normal file
|
@ -0,0 +1,35 @@
|
|||
:global .mo-standalone {
|
||||
top: 34px;
|
||||
bottom: 0px;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background: #DE4320;
|
||||
overflow: hidden;
|
||||
height: 34px;
|
||||
top: -34px;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 24px;
|
||||
padding: 5px 0 5px 10px;
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: block;
|
||||
float: right;
|
||||
width: 150px;
|
||||
line-height: 34px;
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
font-family: sans-serif;
|
||||
cursor: pointer;
|
||||
border-left: 1px solid #972E15;
|
||||
}
|
|
@ -64,6 +64,13 @@ export default class List extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
if (perms.includes('manageFiles')) {
|
||||
actions.push({
|
||||
label: <Icon icon="hdd" title={t('Files')}/>,
|
||||
link: `/templates/${data[0]}/files`
|
||||
});
|
||||
}
|
||||
|
||||
if (perms.includes('share')) {
|
||||
actions.push({
|
||||
label: <Icon icon="share-alt" title={t('Share')}/>,
|
||||
|
|
|
@ -9,6 +9,7 @@ import { Section } from '../lib/page';
|
|||
import TemplatesCUD from './CUD';
|
||||
import TemplatesList from './List';
|
||||
import Share from '../shares/Share';
|
||||
import Files from "../lib/files";
|
||||
|
||||
|
||||
function getMenus(t) {
|
||||
|
@ -31,6 +32,12 @@ function getMenus(t) {
|
|||
visible: resolved => resolved.template.permissions.includes('edit'),
|
||||
panelRender: props => <TemplatesCUD action={props.match.params.action} entity={props.resolved.template} />
|
||||
},
|
||||
files: {
|
||||
title: t('Files'),
|
||||
link: params => `/templates/${params.templateId}/files`,
|
||||
visible: resolved => resolved.template.permissions.includes('edit'),
|
||||
panelRender: props => <Files title={t('Files')} entity={props.resolved.template} entityTypeId="template" />
|
||||
},
|
||||
share: {
|
||||
title: t('Share'),
|
||||
link: params => `/templates/${params.templateId}/share`,
|
||||
|
|
|
@ -15,7 +15,7 @@ module.exports = {
|
|||
rules: [
|
||||
{
|
||||
test: /\.(js|jsx)$/,
|
||||
exclude: /(disposables|react-dnd-touch-backend)/ /* https://github.com/react-dnd/react-dnd/issues/407 */,
|
||||
exclude: /(disposables|react-dnd-touch-backend|attr-accept)/ /* https://github.com/react-dnd/react-dnd/issues/407 */,
|
||||
use: [ 'babel-loader' ]
|
||||
},
|
||||
{
|
||||
|
|
|
@ -208,8 +208,8 @@ permissions=["view", "edit", "delete", "share", "createNamespace", "createList",
|
|||
[roles.namespace.master.children]
|
||||
list=["view", "edit", "delete", "share", "manageFields", "viewSubscriptions", "manageSubscriptions", "manageSegments"]
|
||||
customForm=["view", "edit", "delete", "share"]
|
||||
campaign=["view", "edit", "delete", "share"]
|
||||
template=["view", "edit", "delete", "share"]
|
||||
campaign=["view", "edit", "delete", "share", "manageFiles"]
|
||||
template=["view", "edit", "delete", "share", "manageFiles"]
|
||||
report=["view", "edit", "delete", "share", "execute", "viewContent", "viewOutput"]
|
||||
reportTemplate=["view", "edit", "delete", "share", "execute"]
|
||||
namespace=["view", "edit", "delete", "share", "createNamespace", "createList", "createCustomForm", "createReport", "createReportTemplate", "createTemplate", "manageUsers"]
|
||||
|
@ -227,12 +227,12 @@ permissions=["view", "edit", "delete", "share"]
|
|||
[roles.campaign.master]
|
||||
name="Master"
|
||||
description="All permissions"
|
||||
permissions=["view", "edit", "delete", "share"]
|
||||
permissions=["view", "edit", "delete", "share", "manageFiles"]
|
||||
|
||||
[roles.template.master]
|
||||
name="Master"
|
||||
description="All permissions"
|
||||
permissions=["view", "edit", "delete", "share"]
|
||||
permissions=["view", "edit", "delete", "share", "manageFiles"]
|
||||
|
||||
[roles.report.master]
|
||||
name="Master"
|
||||
|
|
|
@ -1,32 +1,21 @@
|
|||
'use strict';
|
||||
|
||||
const passport = require('./passport');
|
||||
const files = require('../models/files');
|
||||
|
||||
const path = require('path');
|
||||
const uploadedFilesDir = path.join(files.filesDir, 'uploaded');
|
||||
|
||||
function nameToFileName(name) {
|
||||
return name.
|
||||
trim().
|
||||
toLowerCase().
|
||||
replace(/[ .+/]/g, '-').
|
||||
replace(/[^a-z0-9\-_]/gi, '').
|
||||
replace(/--*/g, '-');
|
||||
const multer = require('multer')({
|
||||
dest: uploadedFilesDir
|
||||
});
|
||||
|
||||
function installUploadHandler(router, url, dontReplace = false) {
|
||||
router.postAsync(url, passport.loggedIn, multer.array('files[]'), async (req, res) => {
|
||||
return res.json(await files.createFiles(req.context, req.params.type, req.params.entityId, req.files, dontReplace));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function getReportFileBase(report) {
|
||||
return path.join(__dirname, '..', 'protected', 'reports', report.id + '-' + nameToFileName(report.name));
|
||||
}
|
||||
|
||||
function getReportContentFile(report) {
|
||||
return getReportFileBase(report) + '.out';
|
||||
}
|
||||
|
||||
function getReportOutputFile(report) {
|
||||
return getReportFileBase(report) + '.err';
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
getReportContentFile,
|
||||
getReportOutputFile,
|
||||
nameToFileName
|
||||
installUploadHandler
|
||||
};
|
32
lib/report-helpers.js
Normal file
32
lib/report-helpers.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
|
||||
function nameToFileName(name) {
|
||||
return name.
|
||||
trim().
|
||||
toLowerCase().
|
||||
replace(/[ .+/]/g, '-').
|
||||
replace(/[^a-z0-9\-_]/gi, '').
|
||||
replace(/--*/g, '-');
|
||||
}
|
||||
|
||||
|
||||
function getReportFileBase(report) {
|
||||
return path.join(__dirname, '..', 'protected', 'reports', report.id + '-' + nameToFileName(report.name));
|
||||
}
|
||||
|
||||
function getReportContentFile(report) {
|
||||
return getReportFileBase(report) + '.out';
|
||||
}
|
||||
|
||||
function getReportOutputFile(report) {
|
||||
return getReportFileBase(report) + '.err';
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
getReportContentFile,
|
||||
getReportOutputFile,
|
||||
nameToFileName
|
||||
};
|
119
models/files.js
119
models/files.js
|
@ -6,37 +6,49 @@ const dtHelpers = require('../lib/dt-helpers');
|
|||
const shares = require('./shares');
|
||||
const fs = require('fs-extra-promise');
|
||||
const path = require('path');
|
||||
const interoperableErrors = require('../shared/interoperable-errors');
|
||||
|
||||
const filesDir = path.join(__dirname, '..', 'files');
|
||||
|
||||
const permittedTypes = new Set(['template']);
|
||||
|
||||
function getFilePath(type, entityId, filename) {
|
||||
return path.join(path.join(filesDir, type, id.toString()), filename);
|
||||
return path.join(path.join(filesDir, type, entityId.toString()), filename);
|
||||
}
|
||||
|
||||
function getFilesTable(type) {
|
||||
return 'files_' + type;
|
||||
}
|
||||
|
||||
async function listFilesDTAjax(context, type, entityId, params) {
|
||||
async function listDTAjax(context, type, entityId, params) {
|
||||
enforce(permittedTypes.has(type));
|
||||
await shares.enforceEntityPermission(context, type, entityId, 'edit');
|
||||
await shares.enforceEntityPermission(context, type, entityId, 'manageFiles');
|
||||
return await dtHelpers.ajaxList(
|
||||
params,
|
||||
builder => builder.from(getFilesTable(type)).where({entity: entityId}),
|
||||
['id', 'originalname', 'size', 'created']
|
||||
['id', 'originalname', 'filename', 'size', 'created']
|
||||
);
|
||||
}
|
||||
|
||||
async function list(context, type, entityId) {
|
||||
return await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermission(context, type, entityId, 'view');
|
||||
return await tx(getFilesTable(type)).where({entity: entityId}).select(['id', 'originalname', 'filename', 'size', 'created']).orderBy('originalname', 'asc');
|
||||
});
|
||||
}
|
||||
|
||||
async function getFileById(context, type, id) {
|
||||
enforce(permittedTypes.has(type));
|
||||
const file = await knex.transaction(async tx => {
|
||||
const file = await knex(getFilesTable(type)).where('id', id).first();
|
||||
await shares.enforceEntityPermissionTx(tx, context, type, file.entity, 'edit');
|
||||
const file = await tx(getFilesTable(type)).where('id', id).first();
|
||||
await shares.enforceEntityPermissionTx(tx, context, type, file.entity, 'manageFiles');
|
||||
return file;
|
||||
});
|
||||
|
||||
if (!file) {
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
}
|
||||
|
||||
return {
|
||||
mimetype: file.mimetype,
|
||||
name: file.originalname,
|
||||
|
@ -44,14 +56,18 @@ async function getFileById(context, type, id) {
|
|||
};
|
||||
}
|
||||
|
||||
async function getFileByName(context, type, entityId, name) {
|
||||
async function getFileByFilename(context, type, entityId, name) {
|
||||
enforce(permittedTypes.has(type));
|
||||
const file = await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, type, entityId, 'view');
|
||||
const file = await knex(getFilesTable(type)).where({entity: entityId, originalname: name}).first();
|
||||
const file = await tx(getFilesTable(type)).where({entity: entityId, filename: name}).first();
|
||||
return file;
|
||||
});
|
||||
|
||||
if (!file) {
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
}
|
||||
|
||||
return {
|
||||
mimetype: file.mimetype,
|
||||
name: file.originalname,
|
||||
|
@ -59,57 +75,86 @@ async function getFileByName(context, type, entityId, name) {
|
|||
};
|
||||
}
|
||||
|
||||
async function createFiles(context, type, entityId, files) {
|
||||
async function createFiles(context, type, entityId, files, dontReplace = false) {
|
||||
enforce(permittedTypes.has(type));
|
||||
if (files.length == 0) {
|
||||
// No files uploaded
|
||||
return {uploaded: 0};
|
||||
}
|
||||
|
||||
const fileEntities = [];
|
||||
const filesToMove = [];
|
||||
const ignoredFiles = [];
|
||||
const removedFiles = [];
|
||||
const filesRet = [];
|
||||
|
||||
await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, type, entityId, 'manageFiles');
|
||||
|
||||
const existingNamesRows = await tx(getFilesTable(type)).where('entity', entityId).select(['filename', 'originalname']);
|
||||
const existingNameMap = new Map();
|
||||
for (const row of existingNamesRows) {
|
||||
existingNameMap.set(row.originalname, row);
|
||||
}
|
||||
|
||||
const originalNameSet = new Set();
|
||||
const fileEntities = new Array();
|
||||
const filesToMove = new Array();
|
||||
const ignoredFiles = new Array();
|
||||
|
||||
// Create entities for files
|
||||
for (const file of files) {
|
||||
if (originalNameSet.has(file.originalname)) {
|
||||
const parsedOriginalName = path.parse(file.originalname);
|
||||
let originalName = parsedOriginalName.base;
|
||||
|
||||
if (dontReplace) {
|
||||
let suffix = 1;
|
||||
while (existingNameMap.has(originalName) || originalNameSet.has(originalName)) {
|
||||
originalName = parsedOriginalName.name + '-' + suffix + parsedOriginalName.ext;
|
||||
suffix++;
|
||||
}
|
||||
}
|
||||
|
||||
if (originalNameSet.has(originalName)) {
|
||||
// The file has an original name same as another file
|
||||
ignoredFiles.push(file);
|
||||
} else {
|
||||
originalNameSet.add(file.originalname);
|
||||
|
||||
const fileEntity = {
|
||||
} else {
|
||||
filesToMove.push(file);
|
||||
|
||||
fileEntities.push({
|
||||
entity: entityId,
|
||||
filename: file.filename,
|
||||
originalname: file.originalname,
|
||||
originalname: originalName,
|
||||
mimetype: file.mimetype,
|
||||
encoding: file.encoding,
|
||||
size: file.size
|
||||
};
|
||||
});
|
||||
|
||||
fileEntities.push(fileEntity);
|
||||
filesToMove.push(file);
|
||||
filesRet.push({
|
||||
name: file.filename,
|
||||
originalName: originalName,
|
||||
size: file.size,
|
||||
type: file.mimetype,
|
||||
url: `/files/${type}/${entityId}/${file.filename}`
|
||||
});
|
||||
|
||||
if (existingNameMap.has(originalName)) {
|
||||
removedFiles.push(existingNameMap.get(originalName));
|
||||
}
|
||||
}
|
||||
|
||||
originalNameSet.add(originalName);
|
||||
}
|
||||
|
||||
const originalNameArray = Array.from(originalNameSet);
|
||||
|
||||
const removedFiles = await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, type, entityId, 'edit');
|
||||
const removedFiles = await knex(getFilesTable(type)).where('entity', entityId).whereIn('originalname', originalNameArray).select(['filename', 'originalname']);
|
||||
await knex(getFilesTable(type)).where('entity', entityId).whereIn('originalname', originalNameArray).del();
|
||||
|
||||
if (fileEntities) {
|
||||
await knex(getFilesTable(type)).insert(fileEntities);
|
||||
}
|
||||
return removedFiles;
|
||||
});
|
||||
|
||||
const removedNameSet = new Set();
|
||||
|
||||
// Move new files from upload directory to files directory
|
||||
for (const file of filesToMove) {
|
||||
const filePath = getFilePath(entityId, file.filename);
|
||||
const filePath = getFilePath(type, entityId, file.filename);
|
||||
// The names should be unique, so overwrite is disabled
|
||||
// The directory is created if it does not exist
|
||||
// Empty options argument is passed, otherwise fails
|
||||
|
@ -117,7 +162,6 @@ async function createFiles(context, type, entityId, files) {
|
|||
}
|
||||
// Remove replaced files from files directory
|
||||
for (const file of removedFiles) {
|
||||
removedNameSet.add(file.originalname);
|
||||
const filePath = getFilePath(type, entityId, file.filename);
|
||||
await fs.remove(filePath);
|
||||
}
|
||||
|
@ -128,16 +172,17 @@ async function createFiles(context, type, entityId, files) {
|
|||
|
||||
return {
|
||||
uploaded: files.length,
|
||||
added: fileEntities.length - removedNameSet.size,
|
||||
added: fileEntities.length - removedFiles.length,
|
||||
replaced: removedFiles.length,
|
||||
ignored: ignoredFiles.length
|
||||
ignored: ignoredFiles.length,
|
||||
files: filesRet
|
||||
};
|
||||
}
|
||||
|
||||
async function removeFile(context, type, id) {
|
||||
const file = await knex.transaction(async tx => {
|
||||
const file = await knex(getFilesTable(type)).where('id', id).select('entity', 'filename').first();
|
||||
await shares.enforceEntityPermissionTx(tx, context, type, file.entity, 'edit');
|
||||
const file = await tx(getFilesTable(type)).where('id', id).select('entity', 'filename').first();
|
||||
await shares.enforceEntityPermissionTx(tx, context, type, file.entity, 'manageFiles');
|
||||
await tx(getFilesTable(type)).where('id', id).del();
|
||||
return {filename: file.filename, entity: file.entity};
|
||||
});
|
||||
|
@ -147,9 +192,11 @@ async function removeFile(context, type, id) {
|
|||
}
|
||||
|
||||
module.exports = {
|
||||
listFilesDTAjax,
|
||||
listDTAjax,
|
||||
list,
|
||||
getFileById,
|
||||
getFileByName,
|
||||
getFileByFilename,
|
||||
createFiles,
|
||||
removeFile
|
||||
removeFile,
|
||||
filesDir
|
||||
};
|
|
@ -414,6 +414,10 @@ async function removeDefaultShares(tx, user) {
|
|||
}
|
||||
|
||||
function checkGlobalPermission(context, requiredOperations) {
|
||||
if (!context.user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (context.user.admin) { // This handles the getAdminContext() case
|
||||
return true;
|
||||
}
|
||||
|
@ -443,6 +447,10 @@ function enforceGlobalPermission(context, requiredOperations) {
|
|||
}
|
||||
|
||||
async function _checkPermissionTx(tx, context, entityTypeId, entityId, requiredOperations) {
|
||||
if (!context.user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const entityType = permissions.getEntityType(entityTypeId);
|
||||
|
||||
if (context.user.admin) { // This handles the getAdminContext() case. In this case we don't check the permission, but just the existence.
|
||||
|
@ -530,12 +538,20 @@ async function enforceTypePermissionTx(tx, context, entityTypeId, requiredOperat
|
|||
}
|
||||
|
||||
function getGlobalPermissions(context) {
|
||||
if (!context.user) {
|
||||
return [];
|
||||
}
|
||||
|
||||
enforce(!context.user.admin, 'getPermissions is not supposed to be called by assumed admin');
|
||||
|
||||
return (config.roles.global[context.user.role] || {}).permissions || [];
|
||||
}
|
||||
|
||||
async function getPermissionsTx(tx, context, entityTypeId, entityId) {
|
||||
if (!context.user) {
|
||||
return [];
|
||||
}
|
||||
|
||||
enforce(!context.user.admin, 'getPermissions is not supposed to be called by assumed admin');
|
||||
|
||||
const entityType = permissions.getEntityType(entityTypeId);
|
||||
|
|
28
package-lock.json
generated
28
package-lock.json
generated
|
@ -2376,6 +2376,34 @@
|
|||
"universalify": "0.1.1"
|
||||
}
|
||||
},
|
||||
"fs-extra-promise": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra-promise/-/fs-extra-promise-1.0.1.tgz",
|
||||
"integrity": "sha1-tu0azpexDga5X0WNBRt/BcZhPuY=",
|
||||
"requires": {
|
||||
"bluebird": "3.5.1",
|
||||
"fs-extra": "2.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"fs-extra": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-2.1.2.tgz",
|
||||
"integrity": "sha1-BGxwFjzvmq1GsOSn+kZ/si1x3jU=",
|
||||
"requires": {
|
||||
"graceful-fs": "4.1.11",
|
||||
"jsonfile": "2.4.0"
|
||||
}
|
||||
},
|
||||
"jsonfile": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz",
|
||||
"integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=",
|
||||
"requires": {
|
||||
"graceful-fs": "4.1.11"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
|
|
|
@ -68,6 +68,7 @@
|
|||
"faker": "^4.1.0",
|
||||
"feedparser": "^2.2.7",
|
||||
"fs-extra": "^4.0.2",
|
||||
"fs-extra-promise": "^1.0.1",
|
||||
"geoip-ultralight": "^0.1.5",
|
||||
"gettext-parser": "^1.3.0",
|
||||
"gm": "^1.23.1",
|
||||
|
|
13
routes/files.js
Normal file
13
routes/files.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
'use strict';
|
||||
|
||||
const router = require('../lib/router-async').create();
|
||||
const files = require('../models/files');
|
||||
const contextHelpers = require('../lib/context-helpers');
|
||||
|
||||
router.getAsync('/:type/:entityId/:fileName', async (req, res) => {
|
||||
const file = await files.getFileByFilename(contextHelpers.getAdminContext(), req.params.type, req.params.entityId, req.params.fileName);
|
||||
res.type(file.mimetype);
|
||||
return res.download(file.path, file.name);
|
||||
});
|
||||
|
||||
module.exports = router;
|
|
@ -4,16 +4,139 @@ const config = require('config');
|
|||
const router = require('../lib/router-async').create();
|
||||
const passport = require('../lib/passport');
|
||||
const clientHelpers = require('../lib/client-helpers');
|
||||
const gm = require('gm').subClass({
|
||||
imageMagick: true
|
||||
});
|
||||
|
||||
const bluebird = require('bluebird');
|
||||
const fsReadFile = bluebird.promisify(require('fs').readFile);
|
||||
|
||||
const path = require('path');
|
||||
|
||||
const files = require('../models/files');
|
||||
const fileHelpers = require('../lib/file-helpers');
|
||||
|
||||
// FIXME - add authentication by sandboxToken
|
||||
|
||||
async function placeholderImage(width, height) {
|
||||
const magick = gm(width, height, '#707070');
|
||||
const streamAsync = bluebird.promisify(magick.stream.bind(magick));
|
||||
|
||||
router.getAsync('/editor', passport.csrfProtection, async (req, res) => {
|
||||
const size = 40;
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
|
||||
// stripes
|
||||
while (y < height) {
|
||||
magick
|
||||
.fill('#808080')
|
||||
.drawPolygon([x, y], [x + size, y], [x + size * 2, y + size], [x + size * 2, y + size * 2])
|
||||
.drawPolygon([x, y + size], [x + size, y + size * 2], [x, y + size * 2]);
|
||||
x = x + size * 2;
|
||||
if (x > width) {
|
||||
x = 0;
|
||||
y = y + size * 2;
|
||||
}
|
||||
}
|
||||
|
||||
// text
|
||||
magick
|
||||
.fill('#B0B0B0')
|
||||
.fontSize(20)
|
||||
.drawText(0, 0, width + ' x ' + height, 'center');
|
||||
|
||||
const stream = await streamAsync('png');
|
||||
|
||||
return {
|
||||
format: 'png',
|
||||
stream
|
||||
};
|
||||
}
|
||||
|
||||
async function resizedImage(src, method, width, height) {
|
||||
const filePath = path.join(__dirname, '..', src);
|
||||
|
||||
const magick = gm(filePath);
|
||||
const streamAsync = bluebird.promisify(magick.stream.bind(magick));
|
||||
const formatAsync = bluebird.promisify(magick.format.bind(magick));
|
||||
|
||||
const format = (await formatAsync()).toLowerCase();
|
||||
|
||||
if (method === 'resize') {
|
||||
magick
|
||||
.autoOrient()
|
||||
.resize(width, height);
|
||||
} else if (method === 'cover') {
|
||||
magick
|
||||
.autoOrient()
|
||||
.resize(width, height + '^')
|
||||
.gravity('Center')
|
||||
.extent(width, height + '>');
|
||||
} else {
|
||||
throw new Error(`Method ${method} not supported`);
|
||||
}
|
||||
|
||||
const stream = await streamAsync();
|
||||
|
||||
return {
|
||||
format,
|
||||
stream
|
||||
};
|
||||
}
|
||||
|
||||
function sanitizeSize(val, min, max, defaultVal, allowNull) {
|
||||
if (val === 'null' && allowNull) {
|
||||
return null;
|
||||
}
|
||||
val = Number(val) || defaultVal;
|
||||
val = Math.max(min, val);
|
||||
val = Math.min(max, val);
|
||||
return val;
|
||||
}
|
||||
|
||||
router.getAsync('/img/:type/:fileId', passport.loggedIn, async (req, res) => {
|
||||
const method = req.query.method;
|
||||
const params = req.query.params;
|
||||
let [width, height] = params.split(',');
|
||||
let image;
|
||||
|
||||
if (method === 'placeholder') {
|
||||
width = sanitizeSize(width, 1, 2048, 600, false);
|
||||
height = sanitizeSize(height, 1, 2048, 300, false);
|
||||
image = await placeholderImage(width, height);
|
||||
} else {
|
||||
width = sanitizeSize(width, 1, 2048, 600, false);
|
||||
height = sanitizeSize(height, 1, 2048, 300, true);
|
||||
image = await resizedImage(req.query.src, method, width, height);
|
||||
}
|
||||
|
||||
res.set('Content-Type', 'image/' + image.format);
|
||||
image.stream.pipe(res);
|
||||
});
|
||||
|
||||
|
||||
fileHelpers.installUploadHandler(router, '/upload/:type/:entityId', true);
|
||||
|
||||
router.getAsync('/upload/:type/:fileId', passport.loggedIn, async (req, res) => {
|
||||
const entries = await files.list(req.context, req.params.type, req.params.fileId);
|
||||
|
||||
const filesOut = [];
|
||||
for (const entry of entries) {
|
||||
filesOut.push({
|
||||
name: entry.originalname,
|
||||
url: `/files/${req.params.type}/${req.params.fileId}/${entry.filename}`,
|
||||
size: entry.size,
|
||||
thumbnailUrl: `/files/${req.params.type}/${req.params.fileId}/${entry.filename}` // TODO - use smaller thumbnails
|
||||
})
|
||||
}
|
||||
|
||||
res.json({
|
||||
files: filesOut
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
router.getAsync('/editor', passport.csrfProtection, passport.loggedIn, async (req, res) => {
|
||||
const resourceType = req.query.type;
|
||||
const resourceId = req.query.id;
|
||||
|
||||
|
@ -29,15 +152,6 @@ router.getAsync('/editor', passport.csrfProtection, async (req, res) => {
|
|||
}
|
||||
}
|
||||
|
||||
/* ????
|
||||
resource.editorName = resource.editorName || 'mosaico';
|
||||
resource.editorData = !resource.editorData ?
|
||||
{
|
||||
template: req.query.template || 'versafix-1'
|
||||
} :
|
||||
JSON.parse(resource.editorData);
|
||||
*/
|
||||
|
||||
res.render('mosaico/root', {
|
||||
layout: 'mosaico/layout',
|
||||
editorConfig: config.mosaico,
|
||||
|
@ -47,4 +161,5 @@ router.getAsync('/editor', passport.csrfProtection, async (req, res) => {
|
|||
});
|
||||
});
|
||||
|
||||
|
||||
module.exports = router;
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
const passport = require('../lib/passport');
|
||||
const _ = require('../lib/translate')._;
|
||||
const reports = require('../models/reports');
|
||||
const fileHelpers = require('../lib/file-helpers');
|
||||
const reportHelpers = require('../lib/report-helpers');
|
||||
const shares = require('../models/shares');
|
||||
const contextHelpers = require('../lib/context-helpers');
|
||||
|
||||
|
@ -16,11 +16,11 @@ router.getAsync('/:id/download', passport.loggedIn, async (req, res) => {
|
|||
|
||||
if (report.state == reports.ReportState.FINISHED) {
|
||||
const headers = {
|
||||
'Content-Disposition': 'attachment;filename=' + fileHelpers.nameToFileName(report.name) + '.csv',
|
||||
'Content-Disposition': 'attachment;filename=' + reportHelpers.nameToFileName(report.name) + '.csv',
|
||||
'Content-Type': report.mime_type
|
||||
};
|
||||
|
||||
res.sendFile(fileHelpers.getReportContentFile(report), {headers: headers});
|
||||
res.sendFile(reportHelpers.getReportContentFile(report), {headers: headers});
|
||||
|
||||
} else {
|
||||
return res.status(404).send(_('Report not found'));
|
||||
|
|
|
@ -2,13 +2,11 @@
|
|||
|
||||
const passport = require('../../lib/passport');
|
||||
const files = require('../../models/files');
|
||||
|
||||
const router = require('../../lib/router-async').create();
|
||||
const multer = require('../../lib/multer');
|
||||
const fileHelpers = require('../../lib/file-helpers');
|
||||
|
||||
router.postAsync('/files-table/:type/:entityId', passport.loggedIn, async (req, res) => {
|
||||
const files = await files.listFilesDTAjax(req.context, req.params.type, req.params.entityId, req.body);
|
||||
return res.json(files);
|
||||
return res.json(await files.listDTAjax(req.context, req.params.type, req.params.entityId, req.body));
|
||||
});
|
||||
|
||||
router.getAsync('/files/:type/:fileId', passport.loggedIn, async (req, res) => {
|
||||
|
@ -17,22 +15,11 @@ router.getAsync('/files/:type/:fileId', passport.loggedIn, async (req, res) => {
|
|||
return res.download(file.path, file.name);
|
||||
});
|
||||
|
||||
router.getAsync('/files-by-name/:type/:entityId/:fileName', passport.loggedIn, async (req, res) => {
|
||||
const file = await templates.getFileByName(req.context, req.params.type, req.params.entityId, req.params.fileName);
|
||||
res.type(file.mimetype);
|
||||
// return res.sendFile(file.path); FIXME - remove this comment if the download below is OK
|
||||
return res.download(file.path, file.name);
|
||||
});
|
||||
|
||||
|
||||
router.putAsync('/files/:type/:entityId', passport.loggedIn, multer.array('file'), async (req, res) => {
|
||||
const summary = await files.createFiles(req.context, req.params.type, req.params.entityId, req.files);
|
||||
return res.json(summary);
|
||||
});
|
||||
|
||||
router.deleteAsync('/files/:type/:fileId', passport.loggedIn, async (req, res) => {
|
||||
await files.removeFile(req.context, req.params.type, req.params.fileId);
|
||||
return res.json();
|
||||
});
|
||||
|
||||
fileHelpers.installUploadHandler(router, '/files/:type/:entityId');
|
||||
|
||||
module.exports = router;
|
|
@ -4,7 +4,7 @@ const passport = require('../../lib/passport');
|
|||
const _ = require('../../lib/translate')._;
|
||||
const reports = require('../../models/reports');
|
||||
const reportProcessor = require('../../lib/report-processor');
|
||||
const fileHelpers = require('../../lib/file-helpers');
|
||||
const reportHelpers = require('../../lib/report-helpers');
|
||||
const shares = require('../../models/shares');
|
||||
const contextHelpers = require('../../lib/context-helpers');
|
||||
|
||||
|
@ -62,14 +62,14 @@ router.getAsync('/report-content/:id', async (req, res) => {
|
|||
await shares.enforceEntityPermission(req.context, 'report', req.params.id, 'viewContent');
|
||||
|
||||
const report = await reports.getByIdWithTemplate(contextHelpers.getAdminContext(), req.params.id);
|
||||
res.sendFile(fileHelpers.getReportContentFile(report));
|
||||
res.sendFile(reportHelpers.getReportContentFile(report));
|
||||
});
|
||||
|
||||
router.getAsync('/report-output/:id', async (req, res) => {
|
||||
await shares.enforceEntityPermission(req.context, 'report', req.params.id, 'viewOutput');
|
||||
|
||||
const report = await reports.getByIdWithTemplate(contextHelpers.getAdminContext(), req.params.id);
|
||||
res.sendFile(fileHelpers.getReportOutputFile(report));
|
||||
res.sendFile(reportHelpers.getReportOutputFile(report));
|
||||
});
|
||||
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
that can chroot.
|
||||
*/
|
||||
|
||||
const fileHelpers = require('../lib/file-helpers');
|
||||
const reportHelpers = require('../lib/report-helpers');
|
||||
const fork = require('child_process').fork;
|
||||
const path = require('path');
|
||||
const log = require('npmlog');
|
||||
|
@ -111,7 +111,7 @@ process.on('message', msg => {
|
|||
if (type === 'start-report-processor-worker') {
|
||||
|
||||
const ids = privilegeHelpers.getConfigROUidGid();
|
||||
spawnProcess(msg.tid, path.join(__dirname, '..', 'workers', 'reports', 'report-processor.js'), [msg.data.id], fileHelpers.getReportContentFile(msg.data), fileHelpers.getReportOutputFile(msg.data), path.join(__dirname, '..', 'workers', 'reports'), ids.uid, ids.gid);
|
||||
spawnProcess(msg.tid, path.join(__dirname, '..', 'workers', 'reports', 'report-processor.js'), [msg.data.id], reportHelpers.getReportContentFile(msg.data), reportHelpers.getReportOutputFile(msg.data), path.join(__dirname, '..', 'workers', 'reports'), ids.uid, ids.gid);
|
||||
|
||||
} else if (type === 'stop-process') {
|
||||
const child = processes[msg.tid];
|
||||
|
|
|
@ -22,7 +22,8 @@ class ChangedError extends InteroperableError {
|
|||
|
||||
class NotFoundError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('NotFoundError', msg, data);
|
||||
super('NotFoundError', msg || 'Not Found', data);
|
||||
this.status = 404;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -82,7 +83,7 @@ class NamespaceNotFoundError extends InteroperableError {
|
|||
|
||||
class PermissionDeniedError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('PermissionDeniedError', msg, data);
|
||||
super('PermissionDeniedError', msg || 'Permission Denied', data);
|
||||
this.status = 403;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue