routes refactoring

This commit is contained in:
zadam 2022-12-18 16:12:29 +01:00
parent 6def541e78
commit 2df7d99a91
3 changed files with 195 additions and 183 deletions

View File

@ -31,9 +31,12 @@ class AppContext extends Component {
async start() {
this.initComponents();
// options are often needed for isEnabled()
await options.initializedPromise;
this.renderWidgets();
await Promise.all([froca.initializedPromise, options.initializedPromise]);
await froca.initializedPromise;
this.tabManager.loadTabs();

View File

@ -103,17 +103,24 @@ async function call(method, url, data, headers = {}) {
return resp.body;
}
async function reportError(method, url, status, response) {
async function reportError(method, url, statusCode, response) {
const toastService = (await import("./toast.js")).default;
if ([400, 404].includes(status) && response && typeof response === 'object') {
toastService.showError(response.message);
throw new ValidationError(response);
if (typeof response === 'string') {
try {
response = JSON.parse(response);
}
catch (e) { throw e;}
}
const message = "Error when calling " + method + " " + url + ": " + status + " - " + response;
toastService.showError(message);
toastService.throwError(message);
if ([400, 404].includes(statusCode) && response && typeof response === 'object') {
toastService.showError(response.message);
throw new ValidationError(response);
} else {
const message = "Error when calling " + method + " " + url + ": " + statusCode + " - " + response;
toastService.showError(message);
toastService.throwError(message);
}
}
function ajax(url, method, data, headers) {
@ -137,8 +144,8 @@ function ajax(url, method, data, headers) {
headers: respHeaders
});
},
error: async (jqXhr, status) => {
await reportError(method, url, status, jqXhr.responseText);
error: async jqXhr => {
await reportError(method, url, jqXhr.status, jqXhr.responseText);
rej(jqXhr.responseText);
}

View File

@ -1,11 +1,25 @@
"use strict";
const utils = require('../services/utils');
const multer = require('multer');
const log = require('../services/log');
const express = require('express');
const router = express.Router();
const auth = require('../services/auth');
const cls = require('../services/cls');
const sql = require('../services/sql');
const entityChangesService = require('../services/entity_changes');
const csurf = require('csurf');
const {createPartialContentHandler} = require("express-partial-content");
const rateLimit = require("express-rate-limit");
const AbstractEntity = require("../becca/entities/abstract_entity");
const NotFoundError = require("../errors/not_found_error");
const ValidationError = require("../errors/validation_error");
// page routes
const setupRoute = require('./setup');
const loginRoute = require('./login');
const indexRoute = require('./index');
const utils = require('../services/utils');
const multer = require('multer');
const ValidationError = require("../errors/validation_error");
// API routes
const treeApiRoute = require('./api/tree');
@ -51,183 +65,15 @@ const etapiNoteRoutes = require('../etapi/notes');
const etapiSpecialNoteRoutes = require('../etapi/special_notes');
const etapiSpecRoute = require('../etapi/spec');
const log = require('../services/log');
const express = require('express');
const router = express.Router();
const auth = require('../services/auth');
const cls = require('../services/cls');
const sql = require('../services/sql');
const entityChangesService = require('../services/entity_changes');
const csurf = require('csurf');
const {createPartialContentHandler} = require("express-partial-content");
const rateLimit = require("express-rate-limit");
const AbstractEntity = require("../becca/entities/abstract_entity");
const NotFoundError = require("../errors/not_found_error");
const csrfMiddleware = csurf({
cookie: true,
path: '' // nothing so cookie is valid only for current path
});
/** Handling common patterns. If entity is not caught, serialization to JSON will fail */
function convertEntitiesToPojo(result) {
if (result instanceof AbstractEntity) {
result = result.getPojo();
}
else if (Array.isArray(result)) {
for (const idx in result) {
if (result[idx] instanceof AbstractEntity) {
result[idx] = result[idx].getPojo();
}
}
}
else {
if (result && result.note instanceof AbstractEntity) {
result.note = result.note.getPojo();
}
if (result && result.branch instanceof AbstractEntity) {
result.branch = result.branch.getPojo();
}
}
if (result && result.executionResult) { // from runOnBackend()
result.executionResult = convertEntitiesToPojo(result.executionResult);
}
return result;
}
function apiResultHandler(req, res, result) {
res.setHeader('trilium-max-entity-change-id', entityChangesService.getMaxEntityChangeId());
result = convertEntitiesToPojo(result);
// if it's an array and first element is integer then we consider this to be [statusCode, response] format
if (Array.isArray(result) && result.length > 0 && Number.isInteger(result[0])) {
const [statusCode, response] = result;
if (statusCode !== 200 && statusCode !== 201 && statusCode !== 204) {
log.info(`${req.method} ${req.originalUrl} returned ${statusCode} with response ${JSON.stringify(response)}`);
}
return send(res, statusCode, response);
}
else if (result === undefined) {
return send(res, 204, "");
}
else {
return send(res, 200, result);
}
}
function send(res, statusCode, response) {
if (typeof response === 'string') {
if (statusCode >= 400) {
res.setHeader("Content-Type", "text/plain");
}
res.status(statusCode).send(response);
return response.length;
}
else {
const json = JSON.stringify(response);
res.setHeader("Content-Type", "application/json");
res.status(statusCode).send(json);
return json.length;
}
}
function apiRoute(method, path, routeHandler) {
route(method, path, [auth.checkApiAuth, csrfMiddleware], routeHandler, apiResultHandler);
}
function route(method, path, middleware, routeHandler, resultHandler, transactional = true) {
router[method](path, ...middleware, (req, res, next) => {
const start = Date.now();
try {
cls.namespace.bindEmitter(req);
cls.namespace.bindEmitter(res);
const result = cls.init(() => {
cls.set('componentId', req.headers['trilium-component-id']);
cls.set('localNowDateTime', req.headers['trilium-local-now-datetime']);
cls.set('hoistedNoteId', req.headers['trilium-hoisted-note-id'] || 'root');
const cb = () => routeHandler(req, res, next);
return transactional ? sql.transactional(cb) : cb();
});
if (resultHandler) {
if (result && result.then) {
result
.then(actualResult => {
const responseLength = resultHandler(req, res, actualResult);
log.request(req, res, Date.now() - start, responseLength);
})
.catch(e => handleException(method, path, e, res));
}
else {
const responseLength = resultHandler(req, res, result);
log.request(req, res, Date.now() - start, responseLength);
}
}
}
catch (e) {
handleException(method, path, e, res);
}
});
}
function handleException(method, path, e, res) {
log.error(`${method} ${path} threw exception: ` + e.stack);
if (e instanceof ValidationError) {
res.setHeader("Content-Type", "application/json")
.status(400)
.send({
message: e.message
});
} if (e instanceof NotFoundError) {
res.setHeader("Content-Type", "application/json")
.status(404)
.send({
message: e.message
});
} else {
res.setHeader("Content-Type", "text/plain")
.status(500)
.send(e.message);
}
}
const MAX_ALLOWED_FILE_SIZE_MB = 250;
const GET = 'get', POST = 'post', PUT = 'put', PATCH = 'patch', DELETE = 'delete';
const multerOptions = {
fileFilter: (req, file, cb) => {
// UTF-8 file names are not well decoded by multer/busboy, so we handle the conversion on our side.
// See https://github.com/expressjs/multer/pull/1102.
file.originalname = Buffer.from(file.originalname, "latin1").toString("utf-8");
cb(null, true);
}
};
if (!process.env.TRILIUM_NO_UPLOAD_LIMIT) {
multerOptions.limits = {
fileSize: MAX_ALLOWED_FILE_SIZE_MB * 1024 * 1024
};
}
const uploadMiddleware = multer(multerOptions).single('upload');
const uploadMiddleware = createUploadMiddleware();
const uploadMiddlewareWithErrorHandling = function (req, res, next) {
uploadMiddleware(req, res, function (err) {
@ -250,7 +96,7 @@ function register(app) {
const loginRateLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 10, // limit each IP to 10 requests per windowMs
skipSuccessfulRequests: true // successful auth to rate-limited ETAPI routes isn't counted. However successful auth to /login is still counted!
skipSuccessfulRequests: true // successful auth to rate-limited ETAPI routes isn't counted. However, successful auth to /login is still counted!
});
route(POST, '/login', [loginRateLimiter], loginRoute.login);
@ -471,6 +317,162 @@ function register(app) {
app.use('', router);
}
/** Handling common patterns. If entity is not caught, serialization to JSON will fail */
function convertEntitiesToPojo(result) {
if (result instanceof AbstractEntity) {
result = result.getPojo();
}
else if (Array.isArray(result)) {
for (const idx in result) {
if (result[idx] instanceof AbstractEntity) {
result[idx] = result[idx].getPojo();
}
}
}
else {
if (result && result.note instanceof AbstractEntity) {
result.note = result.note.getPojo();
}
if (result && result.branch instanceof AbstractEntity) {
result.branch = result.branch.getPojo();
}
}
if (result && result.executionResult) { // from runOnBackend()
result.executionResult = convertEntitiesToPojo(result.executionResult);
}
return result;
}
function apiResultHandler(req, res, result) {
res.setHeader('trilium-max-entity-change-id', entityChangesService.getMaxEntityChangeId());
result = convertEntitiesToPojo(result);
// if it's an array and first element is integer then we consider this to be [statusCode, response] format
if (Array.isArray(result) && result.length > 0 && Number.isInteger(result[0])) {
const [statusCode, response] = result;
if (statusCode !== 200 && statusCode !== 201 && statusCode !== 204) {
log.info(`${req.method} ${req.originalUrl} returned ${statusCode} with response ${JSON.stringify(response)}`);
}
return send(res, statusCode, response);
}
else if (result === undefined) {
return send(res, 204, "");
}
else {
return send(res, 200, result);
}
}
function send(res, statusCode, response) {
if (typeof response === 'string') {
if (statusCode >= 400) {
res.setHeader("Content-Type", "text/plain");
}
res.status(statusCode).send(response);
return response.length;
}
else {
const json = JSON.stringify(response);
res.setHeader("Content-Type", "application/json");
res.status(statusCode).send(json);
return json.length;
}
}
function apiRoute(method, path, routeHandler) {
route(method, path, [auth.checkApiAuth, csrfMiddleware], routeHandler, apiResultHandler);
}
function route(method, path, middleware, routeHandler, resultHandler = null, transactional = true) {
router[method](path, ...middleware, (req, res, next) => {
const start = Date.now();
try {
cls.namespace.bindEmitter(req);
cls.namespace.bindEmitter(res);
const result = cls.init(() => {
cls.set('componentId', req.headers['trilium-component-id']);
cls.set('localNowDateTime', req.headers['trilium-local-now-datetime']);
cls.set('hoistedNoteId', req.headers['trilium-hoisted-note-id'] || 'root');
const cb = () => routeHandler(req, res, next);
return transactional ? sql.transactional(cb) : cb();
});
if (!resultHandler) {
return;
}
if (result && result.then) { // promise
result
.then(promiseResult => handleResponse(resultHandler, req, res, promiseResult, start))
.catch(e => handleException(e, method, path, res));
} else {
handleResponse(resultHandler, req, res, result, start)
}
}
catch (e) {
handleException(e, method, path, res);
}
});
}
function handleResponse(resultHandler, req, res, result, start) {
const responseLength = resultHandler(req, res, result);
log.request(req, res, Date.now() - start, responseLength);
}
function handleException(e, method, path, res) {
log.error(`${method} ${path} threw exception: '${e.message}', stack: ${e.stack}`);
if (e instanceof ValidationError) {
res.status(400)
.json({
message: e.message
});
} else if (e instanceof NotFoundError) {
res.status(404)
.json({
message: e.message
});
} else {
res.status(500)
.send(e.message);
}
}
function createUploadMiddleware() {
const multerOptions = {
fileFilter: (req, file, cb) => {
// UTF-8 file names are not well decoded by multer/busboy, so we handle the conversion on our side.
// See https://github.com/expressjs/multer/pull/1102.
file.originalname = Buffer.from(file.originalname, "latin1").toString("utf-8");
cb(null, true);
}
};
if (!process.env.TRILIUM_NO_UPLOAD_LIMIT) {
multerOptions.limits = {
fileSize: MAX_ALLOWED_FILE_SIZE_MB * 1024 * 1024
};
}
return multer(multerOptions).single('upload');
}
module.exports = {
register
};