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() {
|
async start() {
|
||||||
this.initComponents();
|
this.initComponents();
|
||||||
|
|
||||||
|
// options are often needed for isEnabled()
|
||||||
|
await options.initializedPromise;
|
||||||
|
|
||||||
this.renderWidgets();
|
this.renderWidgets();
|
||||||
|
|
||||||
await Promise.all([froca.initializedPromise, options.initializedPromise]);
|
await froca.initializedPromise;
|
||||||
|
|
||||||
this.tabManager.loadTabs();
|
this.tabManager.loadTabs();
|
||||||
|
|
||||||
|
@ -103,17 +103,24 @@ async function call(method, url, data, headers = {}) {
|
|||||||
return resp.body;
|
return resp.body;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function reportError(method, url, status, response) {
|
async function reportError(method, url, statusCode, response) {
|
||||||
const toastService = (await import("./toast.js")).default;
|
const toastService = (await import("./toast.js")).default;
|
||||||
|
|
||||||
if ([400, 404].includes(status) && response && typeof response === 'object') {
|
if (typeof response === 'string') {
|
||||||
toastService.showError(response.message);
|
try {
|
||||||
throw new ValidationError(response);
|
response = JSON.parse(response);
|
||||||
|
}
|
||||||
|
catch (e) { throw e;}
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = "Error when calling " + method + " " + url + ": " + status + " - " + response;
|
if ([400, 404].includes(statusCode) && response && typeof response === 'object') {
|
||||||
toastService.showError(message);
|
toastService.showError(response.message);
|
||||||
toastService.throwError(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) {
|
function ajax(url, method, data, headers) {
|
||||||
@ -137,8 +144,8 @@ function ajax(url, method, data, headers) {
|
|||||||
headers: respHeaders
|
headers: respHeaders
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
error: async (jqXhr, status) => {
|
error: async jqXhr => {
|
||||||
await reportError(method, url, status, jqXhr.responseText);
|
await reportError(method, url, jqXhr.status, jqXhr.responseText);
|
||||||
|
|
||||||
rej(jqXhr.responseText);
|
rej(jqXhr.responseText);
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,25 @@
|
|||||||
"use strict";
|
"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 setupRoute = require('./setup');
|
||||||
const loginRoute = require('./login');
|
const loginRoute = require('./login');
|
||||||
const indexRoute = require('./index');
|
const indexRoute = require('./index');
|
||||||
const utils = require('../services/utils');
|
|
||||||
const multer = require('multer');
|
|
||||||
const ValidationError = require("../errors/validation_error");
|
|
||||||
|
|
||||||
// API routes
|
// API routes
|
||||||
const treeApiRoute = require('./api/tree');
|
const treeApiRoute = require('./api/tree');
|
||||||
@ -51,183 +65,15 @@ const etapiNoteRoutes = require('../etapi/notes');
|
|||||||
const etapiSpecialNoteRoutes = require('../etapi/special_notes');
|
const etapiSpecialNoteRoutes = require('../etapi/special_notes');
|
||||||
const etapiSpecRoute = require('../etapi/spec');
|
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({
|
const csrfMiddleware = csurf({
|
||||||
cookie: true,
|
cookie: true,
|
||||||
path: '' // nothing so cookie is valid only for current path
|
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 MAX_ALLOWED_FILE_SIZE_MB = 250;
|
||||||
|
|
||||||
const GET = 'get', POST = 'post', PUT = 'put', PATCH = 'patch', DELETE = 'delete';
|
const GET = 'get', POST = 'post', PUT = 'put', PATCH = 'patch', DELETE = 'delete';
|
||||||
|
|
||||||
const multerOptions = {
|
const uploadMiddleware = createUploadMiddleware();
|
||||||
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 uploadMiddlewareWithErrorHandling = function (req, res, next) {
|
const uploadMiddlewareWithErrorHandling = function (req, res, next) {
|
||||||
uploadMiddleware(req, res, function (err) {
|
uploadMiddleware(req, res, function (err) {
|
||||||
@ -250,7 +96,7 @@ function register(app) {
|
|||||||
const loginRateLimiter = rateLimit({
|
const loginRateLimiter = rateLimit({
|
||||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||||
max: 10, // limit each IP to 10 requests per windowMs
|
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);
|
route(POST, '/login', [loginRateLimiter], loginRoute.login);
|
||||||
@ -471,6 +317,162 @@ function register(app) {
|
|||||||
app.use('', router);
|
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 = {
|
module.exports = {
|
||||||
register
|
register
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user