server-ts: Port routes/routes

This commit is contained in:
Elian Doran 2024-04-11 23:00:24 +03:00
parent 4638351ec8
commit dededcd303
No known key found for this signature in database
7 changed files with 100 additions and 92 deletions

View File

@ -38,7 +38,7 @@ app.use(sessionParser);
app.use(favicon(`${__dirname}/../images/app-icons/win/icon.ico`)); app.use(favicon(`${__dirname}/../images/app-icons/win/icon.ico`));
require('./routes/assets').register(app); require('./routes/assets').register(app);
require('./routes/routes.js').register(app); require('./routes/routes').register(app);
require('./routes/custom').register(app); require('./routes/custom').register(app);
require('./routes/error_handlers').register(app); require('./routes/error_handlers').register(app);

View File

@ -103,6 +103,6 @@ function register(router: Router) {
}); });
} }
module.exports = { export = {
register register
}; };

View File

@ -229,7 +229,7 @@ function findNotesByUrl(req: Request){
} }
} }
module.exports = { export = {
createNote, createNote,
addClipping, addClipping,
openNote, openNote,

View File

@ -55,6 +55,6 @@ function exportBranch(req: Request, res: Response) {
} }
} }
module.exports = { export = {
exportBranch exportBranch
}; };

View File

@ -124,7 +124,7 @@ function attachmentContentProvider(req: Request) {
return streamContent(attachment.getContent(), attachment.getFileName(), attachment.mime); return streamContent(attachment.getContent(), attachment.getFileName(), attachment.mime);
} }
function streamContent(content: string | Buffer, fileName: string, mimeType: string) { async function streamContent(content: string | Buffer, fileName: string, mimeType: string) {
if (typeof content === "string") { if (typeof content === "string") {
content = Buffer.from(content, 'utf8'); content = Buffer.from(content, 'utf8');
} }

View File

@ -1,87 +1,95 @@
"use strict"; "use strict";
const utils = require('../services/utils'); import utils = require('../services/utils');
const multer = require('multer'); import multer = require('multer');
const log = require('../services/log'); import log = require('../services/log');
const express = require('express'); import express = require('express');
const router = express.Router(); const router = express.Router();
const auth = require('../services/auth'); import auth = require('../services/auth');
const cls = require('../services/cls'); import cls = require('../services/cls');
const sql = require('../services/sql'); import sql = require('../services/sql');
const entityChangesService = require('../services/entity_changes'); import entityChangesService = require('../services/entity_changes');
const csurf = require('csurf'); import csurf = require('csurf');
const { createPartialContentHandler } = require("express-partial-content"); import { createPartialContentHandler } from "express-partial-content";
const rateLimit = require("express-rate-limit"); import rateLimit = require("express-rate-limit");
const AbstractBeccaEntity = require('../becca/entities/abstract_becca_entity'); import AbstractBeccaEntity = require('../becca/entities/abstract_becca_entity');
const NotFoundError = require('../errors/not_found_error'); import NotFoundError = require('../errors/not_found_error');
const ValidationError = require('../errors/validation_error'); import ValidationError = require('../errors/validation_error');
// page routes // page routes
const setupRoute = require('./setup'); import setupRoute = require('./setup');
const loginRoute = require('./login'); import loginRoute = require('./login');
const indexRoute = require('./index'); import indexRoute = require('./index');
// API routes // API routes
const treeApiRoute = require('./api/tree'); import treeApiRoute = require('./api/tree');
const notesApiRoute = require('./api/notes'); import notesApiRoute = require('./api/notes');
const branchesApiRoute = require('./api/branches'); import branchesApiRoute = require('./api/branches');
const attachmentsApiRoute = require('./api/attachments'); import attachmentsApiRoute = require('./api/attachments');
const autocompleteApiRoute = require('./api/autocomplete'); import autocompleteApiRoute = require('./api/autocomplete');
const cloningApiRoute = require('./api/cloning'); import cloningApiRoute = require('./api/cloning');
const revisionsApiRoute = require('./api/revisions'); import revisionsApiRoute = require('./api/revisions');
const recentChangesApiRoute = require('./api/recent_changes'); import recentChangesApiRoute = require('./api/recent_changes');
const optionsApiRoute = require('./api/options'); import optionsApiRoute = require('./api/options');
const passwordApiRoute = require('./api/password'); import passwordApiRoute = require('./api/password');
const syncApiRoute = require('./api/sync'); import syncApiRoute = require('./api/sync');
const loginApiRoute = require('./api/login'); import loginApiRoute = require('./api/login');
const recentNotesRoute = require('./api/recent_notes'); import recentNotesRoute = require('./api/recent_notes');
const appInfoRoute = require('./api/app_info'); import appInfoRoute = require('./api/app_info');
const exportRoute = require('./api/export'); import exportRoute = require('./api/export');
const importRoute = require('./api/import'); import importRoute = require('./api/import');
const setupApiRoute = require('./api/setup'); import setupApiRoute = require('./api/setup');
const sqlRoute = require('./api/sql'); import sqlRoute = require('./api/sql');
const databaseRoute = require('./api/database'); import databaseRoute = require('./api/database');
const imageRoute = require('./api/image'); import imageRoute = require('./api/image');
const attributesRoute = require('./api/attributes'); import attributesRoute = require('./api/attributes');
const scriptRoute = require('./api/script'); import scriptRoute = require('./api/script');
const senderRoute = require('./api/sender'); import senderRoute = require('./api/sender');
const filesRoute = require('./api/files'); import filesRoute = require('./api/files');
const searchRoute = require('./api/search'); import searchRoute = require('./api/search');
const bulkActionRoute = require('./api/bulk_action'); import bulkActionRoute = require('./api/bulk_action');
const specialNotesRoute = require('./api/special_notes'); import specialNotesRoute = require('./api/special_notes');
const noteMapRoute = require('./api/note_map'); import noteMapRoute = require('./api/note_map');
const clipperRoute = require('./api/clipper'); import clipperRoute = require('./api/clipper');
const similarNotesRoute = require('./api/similar_notes'); import similarNotesRoute = require('./api/similar_notes');
const keysRoute = require('./api/keys'); import keysRoute = require('./api/keys');
const backendLogRoute = require('./api/backend_log'); import backendLogRoute = require('./api/backend_log');
const statsRoute = require('./api/stats'); import statsRoute = require('./api/stats');
const fontsRoute = require('./api/fonts'); import fontsRoute = require('./api/fonts');
const etapiTokensApiRoutes = require('./api/etapi_tokens'); import etapiTokensApiRoutes = require('./api/etapi_tokens');
const relationMapApiRoute = require('./api/relation-map'); import relationMapApiRoute = require('./api/relation-map');
const otherRoute = require('./api/other'); import otherRoute = require('./api/other');
const shareRoutes = require('../share/routes'); import shareRoutes = require('../share/routes');
const etapiAuthRoutes = require('../etapi/auth'); import etapiAuthRoutes = require('../etapi/auth');
const etapiAppInfoRoutes = require('../etapi/app_info'); import etapiAppInfoRoutes = require('../etapi/app_info');
const etapiAttachmentRoutes = require('../etapi/attachments'); import etapiAttachmentRoutes = require('../etapi/attachments');
const etapiAttributeRoutes = require('../etapi/attributes'); import etapiAttributeRoutes = require('../etapi/attributes');
const etapiBranchRoutes = require('../etapi/branches'); import etapiBranchRoutes = require('../etapi/branches');
const etapiNoteRoutes = require('../etapi/notes'); import etapiNoteRoutes = require('../etapi/notes');
const etapiSpecialNoteRoutes = require('../etapi/special_notes'); import etapiSpecialNoteRoutes = require('../etapi/special_notes');
const etapiSpecRoute = require('../etapi/spec'); import etapiSpecRoute = require('../etapi/spec');
const etapiBackupRoute = require('../etapi/backup'); import etapiBackupRoute = require('../etapi/backup');
import { RequestHandlerParams } from 'express-serve-static-core';
import { AppRequest, AppRequestHandler } from './route-interface';
const csrfMiddleware = csurf({ const csrfMiddleware = csurf({
cookie: true, cookie: true,
path: '' // empty, so cookie is valid only for the current path path: '' // empty, so cookie is valid only for the current path
}); // TODO: Typescript complains that path does not exist in csurf options, but it's still in the
} as any);
const MAX_ALLOWED_FILE_SIZE_MB = 250; const MAX_ALLOWED_FILE_SIZE_MB = 250;
const GET = 'get', PST = 'post', PUT = 'put', PATCH = 'patch', DEL = 'delete'; const GET = 'get', PST = 'post', PUT = 'put', PATCH = 'patch', DEL = 'delete';
type ApiResultHandler = (req: express.Request, res: express.Response, result: unknown) => number;
// TODO: Deduplicate with etapi_utils.ts afterwards.
type HttpMethod = "all" | "get" | "post" | "put" | "delete" | "patch" | "options" | "head";
const uploadMiddleware = createUploadMiddleware(); const uploadMiddleware = createUploadMiddleware();
const uploadMiddlewareWithErrorHandling = function (req, res, next) { const uploadMiddlewareWithErrorHandling = function (req: express.Request, res: express.Response, next: express.NextFunction) {
uploadMiddleware(req, res, function (err) { uploadMiddleware(req, res, function (err) {
if (err?.code === 'LIMIT_FILE_SIZE') { if (err?.code === 'LIMIT_FILE_SIZE') {
res.setHeader("Content-Type", "text/plain") res.setHeader("Content-Type", "text/plain")
@ -94,12 +102,12 @@ const uploadMiddlewareWithErrorHandling = function (req, res, next) {
}); });
}; };
function register(app) { function register(app: express.Application) {
route(GET, '/', [auth.checkAuth, csrfMiddleware], indexRoute.index); route(GET, '/', [auth.checkAuth, csrfMiddleware], indexRoute.index);
route(GET, '/login', [auth.checkAppInitialized, auth.checkPasswordSet], loginRoute.loginPage); route(GET, '/login', [auth.checkAppInitialized, auth.checkPasswordSet], loginRoute.loginPage);
route(GET, '/set-password', [auth.checkAppInitialized, auth.checkPasswordNotSet], loginRoute.setPasswordPage); route(GET, '/set-password', [auth.checkAppInitialized, auth.checkPasswordNotSet], loginRoute.setPasswordPage);
const loginRateLimiter = rateLimit({ const loginRateLimiter = rateLimit.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!
@ -353,7 +361,7 @@ function register(app) {
} }
/** Handling common patterns. If entity is not caught, serialization to JSON will fail */ /** Handling common patterns. If entity is not caught, serialization to JSON will fail */
function convertEntitiesToPojo(result) { function convertEntitiesToPojo(result: unknown) {
if (result instanceof AbstractBeccaEntity) { if (result instanceof AbstractBeccaEntity) {
result = result.getPojo(); result = result.getPojo();
} }
@ -364,24 +372,24 @@ function convertEntitiesToPojo(result) {
} }
} }
} }
else { else if (result && typeof result === "object") {
if (result && result.note instanceof AbstractBeccaEntity) { if ("note" in result && result.note instanceof AbstractBeccaEntity) {
result.note = result.note.getPojo(); result.note = result.note.getPojo();
} }
if (result && result.branch instanceof AbstractBeccaEntity) { if ("branch" in result && result.branch instanceof AbstractBeccaEntity) {
result.branch = result.branch.getPojo(); result.branch = result.branch.getPojo();
} }
} }
if (result && result.executionResult) { // from runOnBackend() if (result && typeof result === "object" && "executionResult" in result) { // from runOnBackend()
result.executionResult = convertEntitiesToPojo(result.executionResult); result.executionResult = convertEntitiesToPojo(result.executionResult);
} }
return result; return result;
} }
function apiResultHandler(req, res, result) { function apiResultHandler(req: express.Request, res: express.Response, result: unknown) {
res.setHeader('trilium-max-entity-change-id', entityChangesService.getMaxEntityChangeId()); res.setHeader('trilium-max-entity-change-id', entityChangesService.getMaxEntityChangeId());
result = convertEntitiesToPojo(result); result = convertEntitiesToPojo(result);
@ -404,7 +412,7 @@ function apiResultHandler(req, res, result) {
} }
} }
function send(res, statusCode, response) { function send(res: express.Response, statusCode: number, response: unknown) {
if (typeof response === 'string') { if (typeof response === 'string') {
if (statusCode >= 400) { if (statusCode >= 400) {
res.setHeader("Content-Type", "text/plain"); res.setHeader("Content-Type", "text/plain");
@ -424,12 +432,12 @@ function send(res, statusCode, response) {
} }
} }
function apiRoute(method, path, routeHandler) { function apiRoute(method: HttpMethod, path: string, routeHandler: express.Handler) {
route(method, path, [auth.checkApiAuth, csrfMiddleware], routeHandler, apiResultHandler); route(method, path, [auth.checkApiAuth, csrfMiddleware], routeHandler, apiResultHandler);
} }
function route(method, path, middleware, routeHandler, resultHandler = null, transactional = true) { function route(method: HttpMethod, path: string, middleware: (express.Handler | AppRequestHandler)[], routeHandler: AppRequestHandler, resultHandler: ApiResultHandler | null = null, transactional = true) {
router[method](path, ...middleware, (req, res, next) => { router[method](path, ...(middleware as express.Handler[]), (req: express.Request, res: express.Response, next: express.NextFunction) => {
const start = Date.now(); const start = Date.now();
try { try {
@ -441,7 +449,7 @@ function route(method, path, middleware, routeHandler, resultHandler = null, tra
cls.set('localNowDateTime', req.headers['trilium-local-now-datetime']); cls.set('localNowDateTime', req.headers['trilium-local-now-datetime']);
cls.set('hoistedNoteId', req.headers['trilium-hoisted-note-id'] || 'root'); cls.set('hoistedNoteId', req.headers['trilium-hoisted-note-id'] || 'root');
const cb = () => routeHandler(req, res, next); const cb = () => routeHandler(req as AppRequest, res, next);
return transactional ? sql.transactional(cb) : cb(); return transactional ? sql.transactional(cb) : cb();
}); });
@ -452,8 +460,8 @@ function route(method, path, middleware, routeHandler, resultHandler = null, tra
if (result?.then) { // promise if (result?.then) { // promise
result result
.then(promiseResult => handleResponse(resultHandler, req, res, promiseResult, start)) .then((promiseResult: unknown) => handleResponse(resultHandler, req, res, promiseResult, start))
.catch(e => handleException(e, method, path, res)); .catch((e: any) => handleException(e, method, path, res));
} else { } else {
handleResponse(resultHandler, req, res, result, start) handleResponse(resultHandler, req, res, result, start)
} }
@ -464,13 +472,13 @@ function route(method, path, middleware, routeHandler, resultHandler = null, tra
}); });
} }
function handleResponse(resultHandler, req, res, result, start) { function handleResponse(resultHandler: ApiResultHandler, req: express.Request, res: express.Response, result: unknown, start: number) {
const responseLength = resultHandler(req, res, result); const responseLength = resultHandler(req, res, result);
log.request(req, res, Date.now() - start, responseLength); log.request(req, res, Date.now() - start, responseLength);
} }
function handleException(e, method, path, res) { function handleException(e: any, method: HttpMethod, path: string, res: express.Response) {
log.error(`${method} ${path} threw exception: '${e.message}', stack: ${e.stack}`); log.error(`${method} ${path} threw exception: '${e.message}', stack: ${e.stack}`);
if (e instanceof ValidationError) { if (e instanceof ValidationError) {
@ -492,8 +500,8 @@ function handleException(e, method, path, res) {
} }
function createUploadMiddleware() { function createUploadMiddleware() {
const multerOptions = { const multerOptions: multer.Options = {
fileFilter: (req, file, cb) => { fileFilter: (req: express.Request, file, cb) => {
// UTF-8 file names are not well decoded by multer/busboy, so we handle the conversion on our side. // 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. // See https://github.com/expressjs/multer/pull/1102.
file.originalname = Buffer.from(file.originalname, "latin1").toString("utf-8"); file.originalname = Buffer.from(file.originalname, "latin1").toString("utf-8");
@ -510,6 +518,6 @@ function createUploadMiddleware() {
return multer(multerOptions).single('upload'); return multer(multerOptions).single('upload');
} }
module.exports = { export = {
register register
}; };

View File

@ -76,7 +76,7 @@ function error(message: string) {
const requestBlacklist = [ "/libraries", "/app", "/images", "/stylesheets", "/api/recent-notes" ]; const requestBlacklist = [ "/libraries", "/app", "/images", "/stylesheets", "/api/recent-notes" ];
function request(req: Request, res: Response, timeMs: number, responseLength = "?") { function request(req: Request, res: Response, timeMs: number, responseLength: number | string = "?") {
for (const bl of requestBlacklist) { for (const bl of requestBlacklist) {
if (req.url.startsWith(bl)) { if (req.url.startsWith(bl)) {
return; return;