mirror of
https://github.com/zadam/trilium.git
synced 2025-03-01 14:22:32 +01:00
routes refactoring
This commit is contained in:
parent
6def541e78
commit
2df7d99a91
@ -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();
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user