From 2df7d99a91d3aa2f46d4ee6bbc4501d9aedb1da6 Mon Sep 17 00:00:00 2001 From: zadam Date: Sun, 18 Dec 2022 16:12:29 +0100 Subject: [PATCH] routes refactoring --- src/public/app/components/app_context.js | 5 +- src/public/app/services/server.js | 25 +- src/routes/routes.js | 348 ++++++++++++----------- 3 files changed, 195 insertions(+), 183 deletions(-) diff --git a/src/public/app/components/app_context.js b/src/public/app/components/app_context.js index daf27d09a..97b44283d 100644 --- a/src/public/app/components/app_context.js +++ b/src/public/app/components/app_context.js @@ -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(); diff --git a/src/public/app/services/server.js b/src/public/app/services/server.js index 0e76b270b..811b73d6d 100644 --- a/src/public/app/services/server.js +++ b/src/public/app/services/server.js @@ -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); } diff --git a/src/routes/routes.js b/src/routes/routes.js index 8c64c46a1..03d7d1ee5 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -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 };