2025-03-08 17:07:25 +01:00
import { isElectron , safeExtractMessageAndStackFromError } from "../services/utils.js" ;
2024-07-18 21:37:45 +03:00
import multer from "multer" ;
2024-07-18 21:35:17 +03:00
import log from "../services/log.js" ;
2024-07-18 21:37:45 +03:00
import express from "express" ;
2022-12-18 16:12:29 +01:00
const router = express . Router ( ) ;
2024-07-18 21:35:17 +03:00
import auth from "../services/auth.js" ;
2024-09-07 10:21:41 -07:00
import openID from '../services/open_id.js' ;
import totp from './api/totp.js' ;
import recoveryCodes from './api/recovery_codes.js' ;
2024-07-18 21:35:17 +03:00
import cls from "../services/cls.js" ;
import sql from "../services/sql.js" ;
import entityChangesService from "../services/entity_changes.js" ;
2025-01-16 08:25:02 +01:00
import { doubleCsrfProtection as csrfMiddleware } from "./csrf_protection.js" ;
2024-12-13 22:05:05 +02:00
import { createPartialContentHandler } from "@triliumnext/express-partial-content" ;
2024-07-18 21:37:45 +03:00
import rateLimit from "express-rate-limit" ;
2024-07-18 21:35:17 +03:00
import AbstractBeccaEntity from "../becca/entities/abstract_becca_entity.js" ;
import NotFoundError from "../errors/not_found_error.js" ;
import ValidationError from "../errors/validation_error.js" ;
2017-11-03 23:00:35 -04:00
2022-12-18 16:12:29 +01:00
// page routes
2024-07-18 21:35:17 +03:00
import setupRoute from "./setup.js" ;
import loginRoute from "./login.js" ;
import indexRoute from "./index.js" ;
2022-12-18 16:12:29 +01:00
2017-11-03 23:00:35 -04:00
// API routes
2024-07-18 21:35:17 +03:00
import treeApiRoute from "./api/tree.js" ;
import notesApiRoute from "./api/notes.js" ;
import branchesApiRoute from "./api/branches.js" ;
import attachmentsApiRoute from "./api/attachments.js" ;
import autocompleteApiRoute from "./api/autocomplete.js" ;
import cloningApiRoute from "./api/cloning.js" ;
import revisionsApiRoute from "./api/revisions.js" ;
import recentChangesApiRoute from "./api/recent_changes.js" ;
import optionsApiRoute from "./api/options.js" ;
import passwordApiRoute from "./api/password.js" ;
import syncApiRoute from "./api/sync.js" ;
import loginApiRoute from "./api/login.js" ;
import recentNotesRoute from "./api/recent_notes.js" ;
import appInfoRoute from "./api/app_info.js" ;
import exportRoute from "./api/export.js" ;
import importRoute from "./api/import.js" ;
import setupApiRoute from "./api/setup.js" ;
import sqlRoute from "./api/sql.js" ;
import databaseRoute from "./api/database.js" ;
import imageRoute from "./api/image.js" ;
import attributesRoute from "./api/attributes.js" ;
import scriptRoute from "./api/script.js" ;
import senderRoute from "./api/sender.js" ;
import filesRoute from "./api/files.js" ;
import searchRoute from "./api/search.js" ;
import bulkActionRoute from "./api/bulk_action.js" ;
import specialNotesRoute from "./api/special_notes.js" ;
import noteMapRoute from "./api/note_map.js" ;
import clipperRoute from "./api/clipper.js" ;
import similarNotesRoute from "./api/similar_notes.js" ;
import keysRoute from "./api/keys.js" ;
import backendLogRoute from "./api/backend_log.js" ;
import statsRoute from "./api/stats.js" ;
import fontsRoute from "./api/fonts.js" ;
import etapiTokensApiRoutes from "./api/etapi_tokens.js" ;
import relationMapApiRoute from "./api/relation-map.js" ;
import otherRoute from "./api/other.js" ;
import shareRoutes from "../share/routes.js" ;
2024-04-11 23:00:24 +03:00
2024-07-18 21:35:17 +03:00
import etapiAuthRoutes from "../etapi/auth.js" ;
import etapiAppInfoRoutes from "../etapi/app_info.js" ;
import etapiAttachmentRoutes from "../etapi/attachments.js" ;
import etapiAttributeRoutes from "../etapi/attributes.js" ;
import etapiBranchRoutes from "../etapi/branches.js" ;
import etapiNoteRoutes from "../etapi/notes.js" ;
import etapiSpecialNoteRoutes from "../etapi/special_notes.js" ;
import etapiSpecRoute from "../etapi/spec.js" ;
import etapiBackupRoute from "../etapi/backup.js" ;
2025-02-09 21:15:12 +00:00
import apiDocsRoute from "./api_docs.js" ;
2017-11-03 23:00:35 -04:00
2024-09-07 10:21:41 -07:00
2022-09-03 15:11:03 +02:00
const MAX_ALLOWED_FILE_SIZE_MB = 250 ;
2025-01-09 18:07:02 +02:00
const GET = "get" ,
PST = "post" ,
PUT = "put" ,
PATCH = "patch" ,
DEL = "delete" ;
2022-11-17 22:54:45 +01:00
2024-12-10 22:35:23 +02:00
export type ApiResultHandler = ( req : express.Request , res : express.Response , result : unknown ) = > number ;
export type ApiRequestHandler = ( req : express.Request , res : express.Response , next : express.NextFunction ) = > unknown ;
2024-04-11 23:00:24 +03:00
// TODO: Deduplicate with etapi_utils.ts afterwards.
type HttpMethod = "all" | "get" | "post" | "put" | "delete" | "patch" | "options" | "head" ;
2022-12-18 16:12:29 +01:00
const uploadMiddleware = createUploadMiddleware ( ) ;
2022-09-03 15:11:03 +02:00
2024-04-11 23:00:24 +03:00
const uploadMiddlewareWithErrorHandling = function ( req : express.Request , res : express.Response , next : express.NextFunction ) {
2022-09-03 15:11:03 +02:00
uploadMiddleware ( req , res , function ( err ) {
2025-01-09 18:07:02 +02:00
if ( err ? . code === "LIMIT_FILE_SIZE" ) {
res . setHeader ( "Content-Type" , "text/plain" ) . status ( 400 ) . send ( ` Cannot upload file because it excceeded max allowed file size of ${ MAX_ALLOWED_FILE_SIZE_MB } MiB ` ) ;
} else {
2022-09-03 15:11:03 +02:00
next ( ) ;
}
} ) ;
} ;
2018-03-30 12:57:22 -04:00
2024-04-11 23:00:24 +03:00
function register ( app : express.Application ) {
2025-01-09 18:07:02 +02:00
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 ) ;
2021-06-11 21:00:06 +02:00
2024-07-18 21:56:20 +03:00
const loginRateLimiter = rateLimit ( {
2021-06-11 21:00:06 +02:00
windowMs : 15 * 60 * 1000 , // 15 minutes
2022-11-19 18:45:26 +01:00
max : 10 , // limit each IP to 10 requests per windowMs
2022-12-18 16:12:29 +01:00
skipSuccessfulRequests : true // successful auth to rate-limited ETAPI routes isn't counted. However, successful auth to /login is still counted!
2021-06-11 21:00:06 +02:00
} ) ;
2025-01-09 18:07:02 +02:00
route ( PST , "/login" , [ loginRateLimiter ] , loginRoute . login ) ;
route ( PST , "/logout" , [ csrfMiddleware , auth . checkAuth ] , loginRoute . logout ) ;
route ( PST , "/set-password" , [ auth . checkAppInitialized , auth . checkPasswordNotSet ] , loginRoute . setPassword ) ;
route ( GET , "/setup" , [ ] , setupRoute . setupPage ) ;
2017-11-03 23:00:35 -04:00
2024-09-07 10:21:41 -07:00
apiRoute ( GET , '/api/totp/generate' , totp . generateSecret ) ;
apiRoute ( GET , '/api/totp/status' , totp . getTOTPStatus ) ;
apiRoute ( GET , '/api/totp/get' , totp . getSecret ) ;
apiRoute ( GET , '/api/oauth/status' , openID . getOAuthStatus ) ;
apiRoute ( GET , '/api/oauth/validate' , openID . isTokenValid ) ;
apiRoute ( PST , '/api/totp_recovery/set' , recoveryCodes . setRecoveryCodes ) ;
2025-04-02 21:41:48 +02:00
apiRoute ( PST , '/api/totp_recovery/verify' , recoveryCodes . verifyRecoveryCode ) ;
2024-09-07 10:21:41 -07:00
apiRoute ( GET , '/api/totp_recovery/generate' , recoveryCodes . generateRecoveryCodes ) ;
apiRoute ( GET , '/api/totp_recovery/enabled' , recoveryCodes . checkForRecoveryKeys ) ;
apiRoute ( GET , '/api/totp_recovery/used' , recoveryCodes . getUsedRecoveryCodes ) ;
2018-03-30 12:57:22 -04:00
apiRoute ( GET , '/api/tree' , treeApiRoute . getTree ) ;
2023-04-14 16:49:06 +02:00
apiRoute ( PST , '/api/tree/load' , treeApiRoute . load ) ;
2018-04-18 00:26:42 -04:00
2025-01-09 18:07:02 +02:00
apiRoute ( GET , "/api/notes/:noteId" , notesApiRoute . getNote ) ;
apiRoute ( GET , "/api/notes/:noteId/blob" , notesApiRoute . getNoteBlob ) ;
apiRoute ( GET , "/api/notes/:noteId/metadata" , notesApiRoute . getNoteMetadata ) ;
apiRoute ( PUT , "/api/notes/:noteId/data" , notesApiRoute . updateNoteData ) ;
apiRoute ( DEL , "/api/notes/:noteId" , notesApiRoute . deleteNote ) ;
apiRoute ( PUT , "/api/notes/:noteId/undelete" , notesApiRoute . undeleteNote ) ;
apiRoute ( PST , "/api/notes/:noteId/revision" , notesApiRoute . forceSaveRevision ) ;
apiRoute ( PST , "/api/notes/:parentNoteId/children" , notesApiRoute . createNote ) ;
apiRoute ( PUT , "/api/notes/:noteId/sort-children" , notesApiRoute . sortChildNotes ) ;
apiRoute ( PUT , "/api/notes/:noteId/protect/:isProtected" , notesApiRoute . protectNote ) ;
apiRoute ( PUT , "/api/notes/:noteId/type" , notesApiRoute . setNoteTypeMime ) ;
apiRoute ( PUT , "/api/notes/:noteId/title" , notesApiRoute . changeTitle ) ;
apiRoute ( PST , "/api/notes/:noteId/duplicate/:parentNoteId" , notesApiRoute . duplicateSubtree ) ;
apiRoute ( PUT , "/api/notes/:noteId/clone-to-branch/:parentBranchId" , cloningApiRoute . cloneNoteToBranch ) ;
apiRoute ( PUT , "/api/notes/:noteId/toggle-in-parent/:parentNoteId/:present" , cloningApiRoute . toggleNoteInParent ) ;
apiRoute ( PUT , "/api/notes/:noteId/clone-to-note/:parentNoteId" , cloningApiRoute . cloneNoteToParentNote ) ;
apiRoute ( PUT , "/api/notes/:noteId/clone-after/:afterBranchId" , cloningApiRoute . cloneNoteAfter ) ;
route ( PUT , "/api/notes/:noteId/file" , [ auth . checkApiAuthOrElectron , uploadMiddlewareWithErrorHandling , csrfMiddleware ] , filesRoute . updateFile , apiResultHandler ) ;
route ( GET , "/api/notes/:noteId/open" , [ auth . checkApiAuthOrElectron ] , filesRoute . openFile ) ;
route (
GET ,
"/api/notes/:noteId/open-partial" ,
[ auth . checkApiAuthOrElectron ] ,
2021-04-17 22:35:47 +02:00
createPartialContentHandler ( filesRoute . fileContentProvider , {
2025-01-09 18:07:02 +02:00
debug : ( string , extra ) = > {
console . log ( string , extra ) ;
}
} )
) ;
route ( GET , "/api/notes/:noteId/download" , [ auth . checkApiAuthOrElectron ] , filesRoute . downloadFile ) ;
2019-01-27 22:34:41 +01:00
// this "hacky" path is used for easier referencing of CSS resources
2025-01-09 18:07:02 +02:00
route ( GET , "/api/notes/download/:noteId" , [ auth . checkApiAuthOrElectron ] , filesRoute . downloadFile ) ;
apiRoute ( PST , "/api/notes/:noteId/save-to-tmp-dir" , filesRoute . saveNoteToTmpDir ) ;
apiRoute ( PST , "/api/notes/:noteId/upload-modified-file" , filesRoute . uploadModifiedFileToNote ) ;
apiRoute ( PST , "/api/notes/:noteId/convert-to-attachment" , notesApiRoute . convertNoteToAttachment ) ;
apiRoute ( PUT , "/api/branches/:branchId/move-to/:parentBranchId" , branchesApiRoute . moveBranchToParent ) ;
apiRoute ( PUT , "/api/branches/:branchId/move-before/:beforeBranchId" , branchesApiRoute . moveBranchBeforeNote ) ;
apiRoute ( PUT , "/api/branches/:branchId/move-after/:afterBranchId" , branchesApiRoute . moveBranchAfterNote ) ;
apiRoute ( PUT , "/api/branches/:branchId/expanded/:expanded" , branchesApiRoute . setExpanded ) ;
apiRoute ( PUT , "/api/branches/:branchId/expanded-subtree/:expanded" , branchesApiRoute . setExpandedForSubtree ) ;
apiRoute ( DEL , "/api/branches/:branchId" , branchesApiRoute . deleteBranch ) ;
apiRoute ( PUT , "/api/branches/:branchId/set-prefix" , branchesApiRoute . setPrefix ) ;
apiRoute ( GET , "/api/notes/:noteId/attachments" , attachmentsApiRoute . getAttachments ) ;
apiRoute ( PST , "/api/notes/:noteId/attachments" , attachmentsApiRoute . saveAttachment ) ;
route ( PST , "/api/notes/:noteId/attachments/upload" , [ auth . checkApiAuthOrElectron , uploadMiddlewareWithErrorHandling , csrfMiddleware ] , attachmentsApiRoute . uploadAttachment , apiResultHandler ) ;
apiRoute ( GET , "/api/attachments/:attachmentId" , attachmentsApiRoute . getAttachment ) ;
apiRoute ( GET , "/api/attachments/:attachmentId/all" , attachmentsApiRoute . getAllAttachments ) ;
apiRoute ( PST , "/api/attachments/:attachmentId/convert-to-note" , attachmentsApiRoute . convertAttachmentToNote ) ;
apiRoute ( DEL , "/api/attachments/:attachmentId" , attachmentsApiRoute . deleteAttachment ) ;
apiRoute ( PUT , "/api/attachments/:attachmentId/rename" , attachmentsApiRoute . renameAttachment ) ;
apiRoute ( GET , "/api/attachments/:attachmentId/blob" , attachmentsApiRoute . getAttachmentBlob ) ;
route ( GET , "/api/attachments/:attachmentId/image/:filename" , [ auth . checkApiAuthOrElectron ] , imageRoute . returnAttachedImage ) ;
route ( GET , "/api/attachments/:attachmentId/open" , [ auth . checkApiAuthOrElectron ] , filesRoute . openAttachment ) ;
route (
GET ,
"/api/attachments/:attachmentId/open-partial" ,
[ auth . checkApiAuthOrElectron ] ,
2023-05-03 10:23:20 +02:00
createPartialContentHandler ( filesRoute . attachmentContentProvider , {
2025-01-09 18:07:02 +02:00
debug : ( string , extra ) = > {
console . log ( string , extra ) ;
}
} )
) ;
route ( GET , "/api/attachments/:attachmentId/download" , [ auth . checkApiAuthOrElectron ] , filesRoute . downloadAttachment ) ;
2023-05-03 10:23:20 +02:00
// this "hacky" path is used for easier referencing of CSS resources
2025-01-09 18:07:02 +02:00
route ( GET , "/api/attachments/download/:attachmentId" , [ auth . checkApiAuthOrElectron ] , filesRoute . downloadAttachment ) ;
apiRoute ( PST , "/api/attachments/:attachmentId/save-to-tmp-dir" , filesRoute . saveAttachmentToTmpDir ) ;
apiRoute ( PST , "/api/attachments/:attachmentId/upload-modified-file" , filesRoute . uploadModifiedFileToAttachment ) ;
route ( PUT , "/api/attachments/:attachmentId/file" , [ auth . checkApiAuthOrElectron , uploadMiddlewareWithErrorHandling , csrfMiddleware ] , filesRoute . updateAttachment , apiResultHandler ) ;
apiRoute ( GET , "/api/notes/:noteId/revisions" , revisionsApiRoute . getRevisions ) ;
apiRoute ( DEL , "/api/notes/:noteId/revisions" , revisionsApiRoute . eraseAllRevisions ) ;
apiRoute ( PST , "/api/revisions/erase-all-excess-revisions" , revisionsApiRoute . eraseAllExcessRevisions ) ;
apiRoute ( GET , "/api/revisions/:revisionId" , revisionsApiRoute . getRevision ) ;
apiRoute ( GET , "/api/revisions/:revisionId/blob" , revisionsApiRoute . getRevisionBlob ) ;
apiRoute ( DEL , "/api/revisions/:revisionId" , revisionsApiRoute . eraseRevision ) ;
apiRoute ( PST , "/api/revisions/:revisionId/restore" , revisionsApiRoute . restoreRevision ) ;
route ( GET , "/api/revisions/:revisionId/image/:filename" , [ auth . checkApiAuthOrElectron ] , imageRoute . returnImageFromRevision ) ;
route ( GET , "/api/revisions/:revisionId/download" , [ auth . checkApiAuthOrElectron ] , revisionsApiRoute . downloadRevision ) ;
route ( GET , "/api/branches/:branchId/export/:type/:format/:version/:taskId" , [ auth . checkApiAuthOrElectron ] , exportRoute . exportBranch ) ;
route ( PST , "/api/notes/:parentNoteId/notes-import" , [ auth . checkApiAuthOrElectron , uploadMiddlewareWithErrorHandling , csrfMiddleware ] , importRoute . importNotesToBranch , apiResultHandler ) ;
route ( PST , "/api/notes/:parentNoteId/attachments-import" , [ auth . checkApiAuthOrElectron , uploadMiddlewareWithErrorHandling , csrfMiddleware ] , importRoute . importAttachmentsToNote , apiResultHandler ) ;
apiRoute ( GET , "/api/notes/:noteId/attributes" , attributesRoute . getEffectiveNoteAttributes ) ;
apiRoute ( PST , "/api/notes/:noteId/attributes" , attributesRoute . addNoteAttribute ) ;
apiRoute ( PUT , "/api/notes/:noteId/attributes" , attributesRoute . updateNoteAttributes ) ;
apiRoute ( PUT , "/api/notes/:noteId/attribute" , attributesRoute . updateNoteAttribute ) ;
apiRoute ( PUT , "/api/notes/:noteId/set-attribute" , attributesRoute . setNoteAttribute ) ;
apiRoute ( PUT , "/api/notes/:noteId/relations/:name/to/:targetNoteId" , attributesRoute . createRelation ) ;
apiRoute ( DEL , "/api/notes/:noteId/relations/:name/to/:targetNoteId" , attributesRoute . deleteRelation ) ;
apiRoute ( DEL , "/api/notes/:noteId/attributes/:attributeId" , attributesRoute . deleteNoteAttribute ) ;
apiRoute ( GET , "/api/attribute-names" , attributesRoute . getAttributeNames ) ;
apiRoute ( GET , "/api/attribute-values/:attributeName" , attributesRoute . getValuesForAttribute ) ;
2019-04-14 12:18:52 +02:00
2022-09-03 15:11:03 +02:00
// :filename is not used by trilium, but instead used for "save as" to assign a human-readable filename
2025-01-09 18:07:02 +02:00
route ( GET , "/api/images/:noteId/:filename" , [ auth . checkApiAuthOrElectron ] , imageRoute . returnImageFromNote ) ;
route ( PUT , "/api/images/:noteId" , [ auth . checkApiAuthOrElectron , uploadMiddlewareWithErrorHandling , csrfMiddleware ] , imageRoute . updateImage , apiResultHandler ) ;
2018-04-01 20:50:58 -04:00
2025-01-09 18:07:02 +02:00
apiRoute ( GET , "/api/options" , optionsApiRoute . getOptions ) ;
2019-02-17 20:59:52 +01:00
// FIXME: possibly change to sending value in the body to avoid host of HTTP server issues with slashes
2025-04-11 16:51:42 +03:00
apiRoute ( PUT , "/api/options/:name/:value*" , optionsApiRoute . updateOption ) ;
2025-01-09 18:07:02 +02:00
apiRoute ( PUT , "/api/options" , optionsApiRoute . updateOptions ) ;
apiRoute ( GET , "/api/options/user-themes" , optionsApiRoute . getUserThemes ) ;
apiRoute ( GET , "/api/options/codeblock-themes" , optionsApiRoute . getSyntaxHighlightingThemes ) ;
apiRoute ( GET , "/api/options/locales" , optionsApiRoute . getSupportedLocales ) ;
apiRoute ( PST , "/api/password/change" , passwordApiRoute . changePassword ) ;
apiRoute ( PST , "/api/password/reset" , passwordApiRoute . resetPassword ) ;
apiRoute ( PST , "/api/sync/test" , syncApiRoute . testSync ) ;
apiRoute ( PST , "/api/sync/now" , syncApiRoute . syncNow ) ;
apiRoute ( PST , "/api/sync/fill-entity-changes" , syncApiRoute . fillEntityChanges ) ;
apiRoute ( PST , "/api/sync/force-full-sync" , syncApiRoute . forceFullSync ) ;
route ( GET , "/api/sync/check" , [ auth . checkApiAuth ] , syncApiRoute . checkSync , apiResultHandler ) ;
route ( GET , "/api/sync/changed" , [ auth . checkApiAuth ] , syncApiRoute . getChanged , apiResultHandler ) ;
route ( PUT , "/api/sync/update" , [ auth . checkApiAuth ] , syncApiRoute . update , apiResultHandler ) ;
route ( PST , "/api/sync/finished" , [ auth . checkApiAuth ] , syncApiRoute . syncFinished , apiResultHandler ) ;
route ( PST , "/api/sync/check-entity-changes" , [ auth . checkApiAuth ] , syncApiRoute . checkEntityChanges , apiResultHandler ) ;
route ( PST , "/api/sync/queue-sector/:entityName/:sector" , [ auth . checkApiAuth ] , syncApiRoute . queueSector , apiResultHandler ) ;
route ( GET , "/api/sync/stats" , [ ] , syncApiRoute . getStats , apiResultHandler ) ;
apiRoute ( PST , "/api/recent-notes" , recentNotesRoute . addRecentNote ) ;
apiRoute ( GET , "/api/app-info" , appInfoRoute . getAppInfo ) ;
2018-03-30 15:34:07 -04:00
2022-06-19 14:15:31 +02:00
// docker health check
2025-01-09 18:07:02 +02:00
route ( GET , "/api/health-check" , [ ] , ( ) = > ( { status : "ok" } ) , apiResultHandler ) ;
2022-06-19 14:15:31 +02:00
2023-06-30 11:18:34 +02:00
// group of the services below are meant to be executed from the outside
2025-01-09 18:07:02 +02:00
route ( GET , "/api/setup/status" , [ ] , setupApiRoute . getStatus , apiResultHandler ) ;
route ( PST , "/api/setup/new-document" , [ auth . checkAppNotInitialized ] , setupApiRoute . setupNewDocument , apiResultHandler , false ) ;
route ( PST , "/api/setup/sync-from-server" , [ auth . checkAppNotInitialized ] , setupApiRoute . setupSyncFromServer , apiResultHandler , false ) ;
route ( GET , "/api/setup/sync-seed" , [ auth . checkCredentials ] , setupApiRoute . getSyncSeed , apiResultHandler ) ;
route ( PST , "/api/setup/sync-seed" , [ auth . checkAppNotInitialized ] , setupApiRoute . saveSyncSeed , apiResultHandler , false ) ;
apiRoute ( GET , "/api/autocomplete" , autocompleteApiRoute . getAutocomplete ) ;
2025-04-01 21:07:15 +08:00
apiRoute ( GET , "/api/autocomplete/notesCount" , autocompleteApiRoute . getNotesCount ) ;
2025-01-09 18:07:02 +02:00
apiRoute ( GET , "/api/quick-search/:searchString" , searchRoute . quickSearch ) ;
apiRoute ( GET , "/api/search-note/:noteId" , searchRoute . searchFromNote ) ;
apiRoute ( PST , "/api/search-and-execute-note/:noteId" , searchRoute . searchAndExecute ) ;
apiRoute ( PST , "/api/search-related" , searchRoute . getRelatedNotes ) ;
apiRoute ( GET , "/api/search/:searchString" , searchRoute . search ) ;
apiRoute ( GET , "/api/search-templates" , searchRoute . searchTemplates ) ;
apiRoute ( PST , "/api/bulk-action/execute" , bulkActionRoute . execute ) ;
apiRoute ( PST , "/api/bulk-action/affected-notes" , bulkActionRoute . getAffectedNoteCount ) ;
route ( PST , "/api/login/sync" , [ ] , loginApiRoute . loginSync , apiResultHandler ) ;
2023-04-14 16:49:06 +02:00
// this is for entering protected mode so user has to be already logged-in (that's the reason we don't require username)
2025-01-09 18:07:02 +02:00
apiRoute ( PST , "/api/login/protected" , loginApiRoute . loginToProtectedSession ) ;
apiRoute ( PST , "/api/login/protected/touch" , loginApiRoute . touchProtectedSession ) ;
apiRoute ( PST , "/api/logout/protected" , loginApiRoute . logoutFromProtectedSession ) ;
2023-04-14 16:49:06 +02:00
2025-01-09 18:07:02 +02:00
route ( PST , "/api/login/token" , [ loginRateLimiter ] , loginApiRoute . token , apiResultHandler ) ;
2023-04-14 16:49:06 +02:00
2025-01-09 18:07:02 +02:00
apiRoute ( GET , "/api/etapi-tokens" , etapiTokensApiRoutes . getTokens ) ;
apiRoute ( PST , "/api/etapi-tokens" , etapiTokensApiRoutes . createToken ) ;
apiRoute ( PATCH , "/api/etapi-tokens/:etapiTokenId" , etapiTokensApiRoutes . patchToken ) ;
apiRoute ( DEL , "/api/etapi-tokens/:etapiTokenId" , etapiTokensApiRoutes . deleteToken ) ;
2023-04-14 16:49:06 +02:00
// in case of local electron, local calls are allowed unauthenticated, for server they need auth
2025-01-22 18:57:06 +01:00
const clipperMiddleware = isElectron ? [ ] : [ auth . checkEtapiToken ] ;
2023-04-14 16:49:06 +02:00
2025-01-09 18:07:02 +02:00
route ( GET , "/api/clipper/handshake" , clipperMiddleware , clipperRoute . handshake , apiResultHandler ) ;
route ( PST , "/api/clipper/clippings" , clipperMiddleware , clipperRoute . addClipping , apiResultHandler ) ;
route ( PST , "/api/clipper/notes" , clipperMiddleware , clipperRoute . createNote , apiResultHandler ) ;
route ( PST , "/api/clipper/open/:noteId" , clipperMiddleware , clipperRoute . openNote , apiResultHandler ) ;
route ( GET , "/api/clipper/notes-by-url/:noteUrl" , clipperMiddleware , clipperRoute . findNotesByUrl , apiResultHandler ) ;
apiRoute ( GET , "/api/special-notes/inbox/:date" , specialNotesRoute . getInboxNote ) ;
apiRoute ( GET , "/api/special-notes/days/:date" , specialNotesRoute . getDayNote ) ;
2025-04-01 16:25:03 +02:00
apiRoute ( GET , "/api/special-notes/week-first-day/:date" , specialNotesRoute . getWeekFirstDayNote ) ;
apiRoute ( GET , "/api/special-notes/weeks/:week" , specialNotesRoute . getWeekNote ) ;
2025-01-09 18:07:02 +02:00
apiRoute ( GET , "/api/special-notes/months/:month" , specialNotesRoute . getMonthNote ) ;
2025-04-01 19:13:09 +02:00
apiRoute ( GET , "/api/special-notes/quarters/:quarter" , specialNotesRoute . getQuarterNote ) ;
2025-01-09 18:07:02 +02:00
apiRoute ( GET , "/api/special-notes/years/:year" , specialNotesRoute . getYearNote ) ;
apiRoute ( GET , "/api/special-notes/notes-for-month/:month" , specialNotesRoute . getDayNotesForMonth ) ;
apiRoute ( PST , "/api/special-notes/sql-console" , specialNotesRoute . createSqlConsole ) ;
apiRoute ( PST , "/api/special-notes/save-sql-console" , specialNotesRoute . saveSqlConsole ) ;
apiRoute ( PST , "/api/special-notes/search-note" , specialNotesRoute . createSearchNote ) ;
apiRoute ( PST , "/api/special-notes/save-search-note" , specialNotesRoute . saveSearchNote ) ;
apiRoute ( PST , "/api/special-notes/launchers/:noteId/reset" , specialNotesRoute . resetLauncher ) ;
apiRoute ( PST , "/api/special-notes/launchers/:parentNoteId/:launcherType" , specialNotesRoute . createLauncher ) ;
apiRoute ( PUT , "/api/special-notes/api-script-launcher" , specialNotesRoute . createOrUpdateScriptLauncherFromApi ) ;
apiRoute ( GET , "/api/sql/schema" , sqlRoute . getSchema ) ;
apiRoute ( PST , "/api/sql/execute/:noteId" , sqlRoute . execute ) ;
route ( PST , "/api/database/anonymize/:type" , [ auth . checkApiAuthOrElectron , csrfMiddleware ] , databaseRoute . anonymize , apiResultHandler , false ) ;
apiRoute ( GET , "/api/database/anonymized-databases" , databaseRoute . getExistingAnonymizedDatabases ) ;
2018-03-30 17:07:41 -04:00
2024-08-15 00:06:37 +03:00
if ( process . env . TRILIUM_INTEGRATION_TEST === "memory" ) {
2025-01-09 18:07:02 +02:00
route ( PST , "/api/database/rebuild/" , [ auth . checkApiAuthOrElectron ] , databaseRoute . rebuildIntegrationTestDatabase , apiResultHandler , false ) ;
2024-08-15 00:06:37 +03:00
}
2020-05-29 21:55:08 +02:00
// backup requires execution outside of transaction
2025-01-09 18:07:02 +02:00
route ( PST , "/api/database/backup-database" , [ auth . checkApiAuthOrElectron , csrfMiddleware ] , databaseRoute . backupDatabase , apiResultHandler , false ) ;
apiRoute ( GET , "/api/database/backups" , databaseRoute . getExistingBackups ) ;
2020-05-29 21:55:08 +02:00
2018-08-14 18:03:36 +02:00
// VACUUM requires execution outside of transaction
2025-01-09 18:07:02 +02:00
route ( PST , "/api/database/vacuum-database" , [ auth . checkApiAuthOrElectron , csrfMiddleware ] , databaseRoute . vacuumDatabase , apiResultHandler , false ) ;
2018-03-30 17:07:41 -04:00
2025-01-09 18:07:02 +02:00
route ( PST , "/api/database/find-and-fix-consistency-issues" , [ auth . checkApiAuthOrElectron , csrfMiddleware ] , databaseRoute . findAndFixConsistencyIssues , apiResultHandler , false ) ;
2019-12-10 22:03:00 +01:00
2025-01-09 18:07:02 +02:00
apiRoute ( GET , "/api/database/check-integrity" , databaseRoute . checkIntegrity ) ;
2022-02-01 21:22:43 +01:00
2025-01-09 18:07:02 +02:00
route ( PST , "/api/script/exec" , [ auth . checkApiAuth , csrfMiddleware ] , scriptRoute . exec , apiResultHandler , false ) ;
2023-10-29 00:51:23 +02:00
2025-01-09 18:07:02 +02:00
apiRoute ( PST , "/api/script/run/:noteId" , scriptRoute . run ) ;
apiRoute ( GET , "/api/script/startup" , scriptRoute . getStartupBundles ) ;
apiRoute ( GET , "/api/script/widgets" , scriptRoute . getWidgetBundles ) ;
apiRoute ( PST , "/api/script/bundle/:noteId" , scriptRoute . getBundle ) ;
apiRoute ( GET , "/api/script/relation/:noteId/:relationName" , scriptRoute . getRelationBundles ) ;
2018-03-30 17:29:13 -04:00
2019-03-24 22:41:53 +01:00
// no CSRF since this is called from android app
2025-01-09 18:07:02 +02:00
route ( PST , "/api/sender/login" , [ loginRateLimiter ] , loginApiRoute . token , apiResultHandler ) ;
route ( PST , "/api/sender/image" , [ auth . checkEtapiToken , uploadMiddlewareWithErrorHandling ] , senderRoute . uploadImage , apiResultHandler ) ;
route ( PST , "/api/sender/note" , [ auth . checkEtapiToken ] , senderRoute . saveNote , apiResultHandler ) ;
apiRoute ( GET , "/api/keyboard-actions" , keysRoute . getKeyboardActions ) ;
apiRoute ( GET , "/api/keyboard-shortcuts-for-notes" , keysRoute . getShortcutsForNotes ) ;
apiRoute ( PST , "/api/relation-map" , relationMapApiRoute . getRelationMap ) ;
apiRoute ( PST , "/api/notes/erase-deleted-notes-now" , notesApiRoute . eraseDeletedNotesNow ) ;
apiRoute ( PST , "/api/notes/erase-unused-attachments-now" , notesApiRoute . eraseUnusedAttachmentsNow ) ;
apiRoute ( GET , "/api/similar-notes/:noteId" , similarNotesRoute . getSimilarNotes ) ;
apiRoute ( GET , "/api/backend-log" , backendLogRoute . getBackendLog ) ;
apiRoute ( GET , "/api/stats/note-size/:noteId" , statsRoute . getNoteSize ) ;
apiRoute ( GET , "/api/stats/subtree-size/:noteId" , statsRoute . getSubtreeSize ) ;
apiRoute ( PST , "/api/delete-notes-preview" , notesApiRoute . getDeleteNotesPreview ) ;
route ( GET , "/api/fonts" , [ auth . checkApiAuthOrElectron ] , fontsRoute . getFontCss ) ;
apiRoute ( GET , "/api/other/icon-usage" , otherRoute . getIconUsage ) ;
apiRoute ( PST , "/api/other/render-markdown" , otherRoute . renderMarkdown ) ;
apiRoute ( GET , "/api/recent-changes/:ancestorNoteId" , recentChangesApiRoute . getRecentChanges ) ;
apiRoute ( GET , "/api/edited-notes/:date" , revisionsApiRoute . getEditedNotesOnDate ) ;
apiRoute ( PST , "/api/note-map/:noteId/tree" , noteMapRoute . getTreeMap ) ;
apiRoute ( PST , "/api/note-map/:noteId/link" , noteMapRoute . getLinkMap ) ;
apiRoute ( GET , "/api/note-map/:noteId/backlink-count" , noteMapRoute . getBacklinkCount ) ;
apiRoute ( GET , "/api/note-map/:noteId/backlinks" , noteMapRoute . getBacklinks ) ;
2022-01-10 17:09:20 +01:00
2021-10-17 14:44:59 +02:00
shareRoutes . register ( router ) ;
2022-02-01 21:22:43 +01:00
2022-08-22 11:50:58 +02:00
etapiAuthRoutes . register ( router , [ loginRateLimiter ] ) ;
2022-03-07 22:57:48 +01:00
etapiAppInfoRoutes . register ( router ) ;
2023-06-05 09:23:42 +02:00
etapiAttachmentRoutes . register ( router ) ;
2022-01-07 19:33:59 +01:00
etapiAttributeRoutes . register ( router ) ;
etapiBranchRoutes . register ( router ) ;
etapiNoteRoutes . register ( router ) ;
etapiSpecialNoteRoutes . register ( router ) ;
2022-01-07 23:06:04 +01:00
etapiSpecRoute . register ( router ) ;
2023-06-12 23:09:29 +02:00
etapiBackupRoute . register ( router ) ;
2021-10-17 14:44:59 +02:00
2025-02-09 21:15:12 +00:00
// API Documentation
apiDocsRoute . register ( app ) ;
2025-01-09 18:07:02 +02:00
app . use ( "" , router ) ;
2017-11-03 23:00:35 -04:00
}
2022-12-18 16:12:29 +01:00
/** Handling common patterns. If entity is not caught, serialization to JSON will fail */
2024-04-11 23:00:24 +03:00
function convertEntitiesToPojo ( result : unknown ) {
2023-01-03 13:52:37 +01:00
if ( result instanceof AbstractBeccaEntity ) {
2022-12-18 16:12:29 +01:00
result = result . getPojo ( ) ;
2025-01-09 18:07:02 +02:00
} else if ( Array . isArray ( result ) ) {
2022-12-18 16:12:29 +01:00
for ( const idx in result ) {
2023-01-03 13:52:37 +01:00
if ( result [ idx ] instanceof AbstractBeccaEntity ) {
2022-12-18 16:12:29 +01:00
result [ idx ] = result [ idx ] . getPojo ( ) ;
}
}
2025-01-09 18:07:02 +02:00
} else if ( result && typeof result === "object" ) {
2024-04-11 23:00:24 +03:00
if ( "note" in result && result . note instanceof AbstractBeccaEntity ) {
2022-12-18 16:12:29 +01:00
result . note = result . note . getPojo ( ) ;
}
2024-04-11 23:00:24 +03:00
if ( "branch" in result && result . branch instanceof AbstractBeccaEntity ) {
2022-12-18 16:12:29 +01:00
result . branch = result . branch . getPojo ( ) ;
}
}
2025-01-09 18:07:02 +02:00
if ( result && typeof result === "object" && "executionResult" in result ) {
// from runOnBackend()
2022-12-18 16:12:29 +01:00
result . executionResult = convertEntitiesToPojo ( result . executionResult ) ;
}
return result ;
}
2024-04-11 23:00:24 +03:00
function apiResultHandler ( req : express.Request , res : express.Response , result : unknown ) {
2025-01-09 18:07:02 +02:00
res . setHeader ( "trilium-max-entity-change-id" , entityChangesService . getMaxEntityChangeId ( ) ) ;
2022-12-18 16:12:29 +01:00
result = convertEntitiesToPojo ( result ) ;
2023-06-30 11:18:34 +02:00
// if it's an array and the first element is integer, then we consider this to be [statusCode, response] format
2022-12-18 16:12:29 +01:00
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 ) ;
2025-01-09 18:07:02 +02:00
} else if ( result === undefined ) {
2022-12-18 16:12:29 +01:00
return send ( res , 204 , "" ) ;
2025-01-09 18:07:02 +02:00
} else {
2022-12-18 16:12:29 +01:00
return send ( res , 200 , result ) ;
}
}
2024-04-11 23:00:24 +03:00
function send ( res : express.Response , statusCode : number , response : unknown ) {
2025-01-09 18:07:02 +02:00
if ( typeof response === "string" ) {
2022-12-18 16:12:29 +01:00
if ( statusCode >= 400 ) {
res . setHeader ( "Content-Type" , "text/plain" ) ;
}
res . status ( statusCode ) . send ( response ) ;
return response . length ;
2025-01-09 18:07:02 +02:00
} else {
2022-12-18 16:12:29 +01:00
const json = JSON . stringify ( response ) ;
res . setHeader ( "Content-Type" , "application/json" ) ;
res . status ( statusCode ) . send ( json ) ;
return json . length ;
}
}
2024-12-10 22:35:23 +02:00
function apiRoute ( method : HttpMethod , path : string , routeHandler : ApiRequestHandler ) {
2022-12-18 16:12:29 +01:00
route ( method , path , [ auth . checkApiAuth , csrfMiddleware ] , routeHandler , apiResultHandler ) ;
}
2024-12-10 22:35:23 +02:00
function route ( method : HttpMethod , path : string , middleware : express.Handler [ ] , routeHandler : ApiRequestHandler , resultHandler : ApiResultHandler | null = null , transactional = true ) {
2024-04-11 23:00:24 +03:00
router [ method ] ( path , . . . ( middleware as express . Handler [ ] ) , ( req : express.Request , res : express.Response , next : express.NextFunction ) = > {
2022-12-18 16:12:29 +01:00
const start = Date . now ( ) ;
try {
cls . namespace . bindEmitter ( req ) ;
cls . namespace . bindEmitter ( res ) ;
const result = cls . init ( ( ) = > {
2025-01-09 18:07:02 +02:00
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" ) ;
2022-12-18 16:12:29 +01:00
2024-12-10 22:35:23 +02:00
const cb = ( ) = > routeHandler ( req , res , next ) ;
2022-12-18 16:12:29 +01:00
return transactional ? sql . transactional ( cb ) : cb ( ) ;
} ) ;
if ( ! resultHandler ) {
return ;
}
2025-01-09 18:07:02 +02:00
if ( result ? . then ) {
// promise
2025-03-08 16:01:53 +01:00
result . then ( ( promiseResult : unknown ) = > handleResponse ( resultHandler , req , res , promiseResult , start ) ) . catch ( ( e : unknown ) = > handleException ( e , method , path , res ) ) ;
2022-12-18 16:12:29 +01:00
} else {
2025-01-09 18:07:02 +02:00
handleResponse ( resultHandler , req , res , result , start ) ;
2022-12-18 16:12:29 +01:00
}
2025-01-09 18:07:02 +02:00
} catch ( e ) {
2022-12-18 16:12:29 +01:00
handleException ( e , method , path , res ) ;
}
} ) ;
}
2024-04-11 23:00:24 +03:00
function handleResponse ( resultHandler : ApiResultHandler , req : express.Request , res : express.Response , result : unknown , start : number ) {
2022-12-18 16:12:29 +01:00
const responseLength = resultHandler ( req , res , result ) ;
log . request ( req , res , Date . now ( ) - start , responseLength ) ;
}
2025-03-07 22:27:13 +01:00
function handleException ( e : unknown | Error , method : HttpMethod , path : string , res : express.Response ) {
2025-03-08 17:07:25 +01:00
const [ errMessage , errStack ] = safeExtractMessageAndStackFromError ( e ) ;
2025-03-07 22:27:13 +01:00
log . error ( ` ${ method } ${ path } threw exception: ' ${ errMessage } ', stack: ${ errStack } ` ) ;
2025-03-22 12:35:00 +01:00
const resStatusCode = ( e instanceof ValidationError || e instanceof NotFoundError ) ? e.statusCode : 500 ;
2025-03-07 22:27:13 +01:00
res . status ( resStatusCode ) . json ( {
message : errMessage
} ) ;
2022-12-18 16:12:29 +01:00
}
function createUploadMiddleware() {
2024-04-11 23:00:24 +03:00
const multerOptions : multer.Options = {
fileFilter : ( req : express.Request , file , cb ) = > {
2022-12-18 16:12:29 +01:00
// 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
} ;
}
2025-01-09 18:07:02 +02:00
return multer ( multerOptions ) . single ( "upload" ) ;
2022-12-18 16:12:29 +01:00
}
2024-07-18 21:42:44 +03:00
export default {
2017-11-03 23:00:35 -04:00
register
2020-05-29 21:55:08 +02:00
} ;