|
|
|
|
@@ -1,87 +1,95 @@
|
|
|
|
|
"use strict";
|
|
|
|
|
|
|
|
|
|
const utils = require('../services/utils');
|
|
|
|
|
const multer = require('multer');
|
|
|
|
|
const log = require('../services/log');
|
|
|
|
|
const express = require('express');
|
|
|
|
|
import utils = require('../services/utils');
|
|
|
|
|
import multer = require('multer');
|
|
|
|
|
import log = require('../services/log');
|
|
|
|
|
import 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 AbstractBeccaEntity = require('../becca/entities/abstract_becca_entity');
|
|
|
|
|
const NotFoundError = require('../errors/not_found_error');
|
|
|
|
|
const ValidationError = require('../errors/validation_error');
|
|
|
|
|
import auth = require('../services/auth');
|
|
|
|
|
import cls = require('../services/cls');
|
|
|
|
|
import sql = require('../services/sql');
|
|
|
|
|
import entityChangesService = require('../services/entity_changes');
|
|
|
|
|
import csurf = require('csurf');
|
|
|
|
|
import { createPartialContentHandler } from "express-partial-content";
|
|
|
|
|
import rateLimit = require("express-rate-limit");
|
|
|
|
|
import AbstractBeccaEntity = require('../becca/entities/abstract_becca_entity');
|
|
|
|
|
import NotFoundError = require('../errors/not_found_error');
|
|
|
|
|
import ValidationError = require('../errors/validation_error');
|
|
|
|
|
|
|
|
|
|
// page routes
|
|
|
|
|
const setupRoute = require('./setup');
|
|
|
|
|
const loginRoute = require('./login');
|
|
|
|
|
const indexRoute = require('./index');
|
|
|
|
|
import setupRoute = require('./setup');
|
|
|
|
|
import loginRoute = require('./login');
|
|
|
|
|
import indexRoute = require('./index');
|
|
|
|
|
|
|
|
|
|
// API routes
|
|
|
|
|
const treeApiRoute = require('./api/tree');
|
|
|
|
|
const notesApiRoute = require('./api/notes');
|
|
|
|
|
const branchesApiRoute = require('./api/branches');
|
|
|
|
|
const attachmentsApiRoute = require('./api/attachments');
|
|
|
|
|
const autocompleteApiRoute = require('./api/autocomplete');
|
|
|
|
|
const cloningApiRoute = require('./api/cloning');
|
|
|
|
|
const revisionsApiRoute = require('./api/revisions');
|
|
|
|
|
const recentChangesApiRoute = require('./api/recent_changes');
|
|
|
|
|
const optionsApiRoute = require('./api/options');
|
|
|
|
|
const passwordApiRoute = require('./api/password');
|
|
|
|
|
const syncApiRoute = require('./api/sync');
|
|
|
|
|
const loginApiRoute = require('./api/login');
|
|
|
|
|
const recentNotesRoute = require('./api/recent_notes');
|
|
|
|
|
const appInfoRoute = require('./api/app_info');
|
|
|
|
|
const exportRoute = require('./api/export');
|
|
|
|
|
const importRoute = require('./api/import');
|
|
|
|
|
const setupApiRoute = require('./api/setup');
|
|
|
|
|
const sqlRoute = require('./api/sql');
|
|
|
|
|
const databaseRoute = require('./api/database');
|
|
|
|
|
const imageRoute = require('./api/image');
|
|
|
|
|
const attributesRoute = require('./api/attributes');
|
|
|
|
|
const scriptRoute = require('./api/script');
|
|
|
|
|
const senderRoute = require('./api/sender');
|
|
|
|
|
const filesRoute = require('./api/files');
|
|
|
|
|
const searchRoute = require('./api/search');
|
|
|
|
|
const bulkActionRoute = require('./api/bulk_action');
|
|
|
|
|
const specialNotesRoute = require('./api/special_notes');
|
|
|
|
|
const noteMapRoute = require('./api/note_map');
|
|
|
|
|
const clipperRoute = require('./api/clipper');
|
|
|
|
|
const similarNotesRoute = require('./api/similar_notes');
|
|
|
|
|
const keysRoute = require('./api/keys');
|
|
|
|
|
const backendLogRoute = require('./api/backend_log');
|
|
|
|
|
const statsRoute = require('./api/stats');
|
|
|
|
|
const fontsRoute = require('./api/fonts');
|
|
|
|
|
const etapiTokensApiRoutes = require('./api/etapi_tokens');
|
|
|
|
|
const relationMapApiRoute = require('./api/relation-map');
|
|
|
|
|
const otherRoute = require('./api/other');
|
|
|
|
|
const shareRoutes = require('../share/routes');
|
|
|
|
|
import treeApiRoute = require('./api/tree');
|
|
|
|
|
import notesApiRoute = require('./api/notes');
|
|
|
|
|
import branchesApiRoute = require('./api/branches');
|
|
|
|
|
import attachmentsApiRoute = require('./api/attachments');
|
|
|
|
|
import autocompleteApiRoute = require('./api/autocomplete');
|
|
|
|
|
import cloningApiRoute = require('./api/cloning');
|
|
|
|
|
import revisionsApiRoute = require('./api/revisions');
|
|
|
|
|
import recentChangesApiRoute = require('./api/recent_changes');
|
|
|
|
|
import optionsApiRoute = require('./api/options');
|
|
|
|
|
import passwordApiRoute = require('./api/password');
|
|
|
|
|
import syncApiRoute = require('./api/sync');
|
|
|
|
|
import loginApiRoute = require('./api/login');
|
|
|
|
|
import recentNotesRoute = require('./api/recent_notes');
|
|
|
|
|
import appInfoRoute = require('./api/app_info');
|
|
|
|
|
import exportRoute = require('./api/export');
|
|
|
|
|
import importRoute = require('./api/import');
|
|
|
|
|
import setupApiRoute = require('./api/setup');
|
|
|
|
|
import sqlRoute = require('./api/sql');
|
|
|
|
|
import databaseRoute = require('./api/database');
|
|
|
|
|
import imageRoute = require('./api/image');
|
|
|
|
|
import attributesRoute = require('./api/attributes');
|
|
|
|
|
import scriptRoute = require('./api/script');
|
|
|
|
|
import senderRoute = require('./api/sender');
|
|
|
|
|
import filesRoute = require('./api/files');
|
|
|
|
|
import searchRoute = require('./api/search');
|
|
|
|
|
import bulkActionRoute = require('./api/bulk_action');
|
|
|
|
|
import specialNotesRoute = require('./api/special_notes');
|
|
|
|
|
import noteMapRoute = require('./api/note_map');
|
|
|
|
|
import clipperRoute = require('./api/clipper');
|
|
|
|
|
import similarNotesRoute = require('./api/similar_notes');
|
|
|
|
|
import keysRoute = require('./api/keys');
|
|
|
|
|
import backendLogRoute = require('./api/backend_log');
|
|
|
|
|
import statsRoute = require('./api/stats');
|
|
|
|
|
import fontsRoute = require('./api/fonts');
|
|
|
|
|
import etapiTokensApiRoutes = require('./api/etapi_tokens');
|
|
|
|
|
import relationMapApiRoute = require('./api/relation-map');
|
|
|
|
|
import otherRoute = require('./api/other');
|
|
|
|
|
import shareRoutes = require('../share/routes');
|
|
|
|
|
|
|
|
|
|
const etapiAuthRoutes = require('../etapi/auth');
|
|
|
|
|
const etapiAppInfoRoutes = require('../etapi/app_info');
|
|
|
|
|
const etapiAttachmentRoutes = require('../etapi/attachments');
|
|
|
|
|
const etapiAttributeRoutes = require('../etapi/attributes');
|
|
|
|
|
const etapiBranchRoutes = require('../etapi/branches');
|
|
|
|
|
const etapiNoteRoutes = require('../etapi/notes');
|
|
|
|
|
const etapiSpecialNoteRoutes = require('../etapi/special_notes');
|
|
|
|
|
const etapiSpecRoute = require('../etapi/spec');
|
|
|
|
|
const etapiBackupRoute = require('../etapi/backup');
|
|
|
|
|
import etapiAuthRoutes = require('../etapi/auth');
|
|
|
|
|
import etapiAppInfoRoutes = require('../etapi/app_info');
|
|
|
|
|
import etapiAttachmentRoutes = require('../etapi/attachments');
|
|
|
|
|
import etapiAttributeRoutes = require('../etapi/attributes');
|
|
|
|
|
import etapiBranchRoutes = require('../etapi/branches');
|
|
|
|
|
import etapiNoteRoutes = require('../etapi/notes');
|
|
|
|
|
import etapiSpecialNoteRoutes = require('../etapi/special_notes');
|
|
|
|
|
import etapiSpecRoute = require('../etapi/spec');
|
|
|
|
|
import etapiBackupRoute = require('../etapi/backup');
|
|
|
|
|
import { RequestHandlerParams } from 'express-serve-static-core';
|
|
|
|
|
import { AppRequest, AppRequestHandler } from './route-interface';
|
|
|
|
|
|
|
|
|
|
const csrfMiddleware = csurf({
|
|
|
|
|
cookie: true,
|
|
|
|
|
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 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 uploadMiddlewareWithErrorHandling = function (req, res, next) {
|
|
|
|
|
const uploadMiddlewareWithErrorHandling = function (req: express.Request, res: express.Response, next: express.NextFunction) {
|
|
|
|
|
uploadMiddleware(req, res, function (err) {
|
|
|
|
|
if (err?.code === 'LIMIT_FILE_SIZE') {
|
|
|
|
|
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, '/login', [auth.checkAppInitialized, auth.checkPasswordSet], loginRoute.loginPage);
|
|
|
|
|
route(GET, '/set-password', [auth.checkAppInitialized, auth.checkPasswordNotSet], loginRoute.setPasswordPage);
|
|
|
|
|
|
|
|
|
|
const loginRateLimiter = rateLimit({
|
|
|
|
|
const loginRateLimiter = rateLimit.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!
|
|
|
|
|
@@ -353,7 +361,7 @@ function register(app) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Handling common patterns. If entity is not caught, serialization to JSON will fail */
|
|
|
|
|
function convertEntitiesToPojo(result) {
|
|
|
|
|
function convertEntitiesToPojo(result: unknown) {
|
|
|
|
|
if (result instanceof AbstractBeccaEntity) {
|
|
|
|
|
result = result.getPojo();
|
|
|
|
|
}
|
|
|
|
|
@@ -364,24 +372,24 @@ function convertEntitiesToPojo(result) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
if (result && result.note instanceof AbstractBeccaEntity) {
|
|
|
|
|
else if (result && typeof result === "object") {
|
|
|
|
|
if ("note" in result && result.note instanceof AbstractBeccaEntity) {
|
|
|
|
|
result.note = result.note.getPojo();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (result && result.branch instanceof AbstractBeccaEntity) {
|
|
|
|
|
if ("branch" in result && result.branch instanceof AbstractBeccaEntity) {
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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());
|
|
|
|
|
|
|
|
|
|
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 (statusCode >= 400) {
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function route(method, path, middleware, routeHandler, resultHandler = null, transactional = true) {
|
|
|
|
|
router[method](path, ...middleware, (req, res, next) => {
|
|
|
|
|
function route(method: HttpMethod, path: string, middleware: (express.Handler | AppRequestHandler)[], routeHandler: AppRequestHandler, resultHandler: ApiResultHandler | null = null, transactional = true) {
|
|
|
|
|
router[method](path, ...(middleware as express.Handler[]), (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
|
|
|
|
const start = Date.now();
|
|
|
|
|
|
|
|
|
|
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('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();
|
|
|
|
|
});
|
|
|
|
|
@@ -452,8 +460,8 @@ function route(method, path, middleware, routeHandler, resultHandler = null, tra
|
|
|
|
|
|
|
|
|
|
if (result?.then) { // promise
|
|
|
|
|
result
|
|
|
|
|
.then(promiseResult => handleResponse(resultHandler, req, res, promiseResult, start))
|
|
|
|
|
.catch(e => handleException(e, method, path, res));
|
|
|
|
|
.then((promiseResult: unknown) => handleResponse(resultHandler, req, res, promiseResult, start))
|
|
|
|
|
.catch((e: any) => handleException(e, method, path, res));
|
|
|
|
|
} else {
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
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}`);
|
|
|
|
|
|
|
|
|
|
if (e instanceof ValidationError) {
|
|
|
|
|
@@ -492,8 +500,8 @@ function handleException(e, method, path, res) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createUploadMiddleware() {
|
|
|
|
|
const multerOptions = {
|
|
|
|
|
fileFilter: (req, file, cb) => {
|
|
|
|
|
const multerOptions: multer.Options = {
|
|
|
|
|
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.
|
|
|
|
|
// See https://github.com/expressjs/multer/pull/1102.
|
|
|
|
|
file.originalname = Buffer.from(file.originalname, "latin1").toString("utf-8");
|
|
|
|
|
@@ -510,6 +518,6 @@ function createUploadMiddleware() {
|
|
|
|
|
return multer(multerOptions).single('upload');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
|
export = {
|
|
|
|
|
register
|
|
|
|
|
};
|