diff --git a/src/etapi/etapi_utils.ts b/src/etapi/etapi_utils.ts index 4880bed86..bcb589325 100644 --- a/src/etapi/etapi_utils.ts +++ b/src/etapi/etapi_utils.ts @@ -5,6 +5,7 @@ import becca = require('../becca/becca'); import etapiTokenService = require('../services/etapi_tokens'); import config = require('../services/config'); import { NextFunction, Request, RequestHandler, Response, Router } from 'express'; +import { AppRequest, AppRequestHandler } from '../routes/route-interface'; const GENERIC_CODE = "GENERIC"; type HttpMethod = "all" | "get" | "post" | "put" | "delete" | "patch" | "options" | "head"; @@ -44,7 +45,7 @@ function checkEtapiAuth(req: Request, res: Response, next: NextFunction) { } } -function processRequest(req: Request, res: Response, routeHandler: RequestHandler, next: NextFunction, method: string, path: string) { +function processRequest(req: Request, res: Response, routeHandler: AppRequestHandler, next: NextFunction, method: string, path: string) { try { cls.namespace.bindEmitter(req); cls.namespace.bindEmitter(res); @@ -53,7 +54,7 @@ function processRequest(req: Request, res: Response, routeHandler: RequestHandle cls.set('componentId', "etapi"); cls.set('localNowDateTime', req.headers['trilium-local-now-datetime']); - const cb = () => routeHandler(req, res, next); + const cb = () => routeHandler(req as AppRequest, res, next); return sql.transactional(cb); }); @@ -68,7 +69,7 @@ function processRequest(req: Request, res: Response, routeHandler: RequestHandle } } -function route(router: Router, method: HttpMethod, path: string, routeHandler: RequestHandler) { +function route(router: Router, method: HttpMethod, path: string, routeHandler: AppRequestHandler) { router[method](path, checkEtapiAuth, (req: Request, res: Response, next: NextFunction) => processRequest(req, res, routeHandler, next, method, path)); } diff --git a/src/etapi/notes.js b/src/etapi/notes.ts similarity index 76% rename from src/etapi/notes.js rename to src/etapi/notes.ts index 71485388e..fcb2d314f 100644 --- a/src/etapi/notes.js +++ b/src/etapi/notes.ts @@ -1,20 +1,26 @@ -const becca = require('../becca/becca'); -const utils = require('../services/utils'); -const eu = require('./etapi_utils'); -const mappers = require('./mappers'); -const noteService = require('../services/notes'); -const TaskContext = require('../services/task_context'); -const v = require('./validators'); -const searchService = require('../services/search/services/search'); -const SearchContext = require('../services/search/search_context'); -const zipExportService = require('../services/export/zip'); -const zipImportService = require('../services/import/zip'); +import becca = require('../becca/becca'); +import utils = require('../services/utils'); +import eu = require('./etapi_utils'); +import mappers = require('./mappers'); +import noteService = require('../services/notes'); +import TaskContext = require('../services/task_context'); +import v = require('./validators'); +import searchService = require('../services/search/services/search'); +import SearchContext = require('../services/search/search_context'); +import zipExportService = require('../services/export/zip'); +import zipImportService = require('../services/import/zip'); +import { Router } from 'express'; +import { AppRequest } from '../routes/route-interface'; +import { ParsedQs } from 'qs'; +import { NoteParams } from '../services/note-interface'; +import BNote = require('../becca/entities/bnote'); +import { SearchParams } from '../services/search/services/types'; -function register(router) { +function register(router: Router) { eu.route(router, 'get', '/etapi/notes', (req, res, next) => { const { search } = req.query; - if (!search?.trim()) { + if (typeof search !== "string" || !search?.trim()) { throw new eu.EtapiError(400, 'SEARCH_QUERY_PARAM_MANDATORY', "'search' query parameter is mandatory."); } @@ -24,8 +30,8 @@ function register(router) { const searchResults = searchService.findResultsWithQuery(search, searchContext); const foundNotes = searchResults.map(sr => becca.notes[sr.noteId]); - const resp = { - results: foundNotes.map(note => mappers.mapNoteToPojo(note)) + const resp: any = { + results: foundNotes.map(note => mappers.mapNoteToPojo(note)), }; if (searchContext.debugInfo) { @@ -41,7 +47,7 @@ function register(router) { res.json(mappers.mapNoteToPojo(note)); }); - const ALLOWED_PROPERTIES_FOR_CREATE_NOTE = { + const ALLOWED_PROPERTIES_FOR_CREATE_NOTE: ValidatorMap = { 'parentNoteId': [v.mandatory, v.notNull, v.isNoteId], 'title': [v.mandatory, v.notNull, v.isString], 'type': [v.mandatory, v.notNull, v.isNoteType], @@ -56,9 +62,9 @@ function register(router) { }; eu.route(router, 'post', '/etapi/create-note', (req, res, next) => { - const params = {}; - - eu.validateAndPatch(params, req.body, ALLOWED_PROPERTIES_FOR_CREATE_NOTE); + const _params = {}; + eu.validateAndPatch(_params, req.body, ALLOWED_PROPERTIES_FOR_CREATE_NOTE); + const params = _params as NoteParams; try { const resp = noteService.createNewNote(params); @@ -68,7 +74,7 @@ function register(router) { branch: mappers.mapBranchToPojo(resp.branch) }); } - catch (e) { + catch (e: any) { return eu.sendError(res, 500, eu.GENERIC_CODE, e.message); } }); @@ -143,7 +149,7 @@ function register(router) { const note = eu.getAndCheckNote(req.params.noteId); const format = req.query.format || "html"; - if (!["html", "markdown"].includes(format)) { + if (typeof format !== "string" || !["html", "markdown"].includes(format)) { throw new eu.EtapiError(400, "UNRECOGNIZED_EXPORT_FORMAT", `Unrecognized export format '${format}', supported values are 'html' (default) or 'markdown'.`); } @@ -153,7 +159,7 @@ function register(router) { // (e.g. branchIds are not seen in UI), that we export "note export" instead. const branch = note.getParentBranches()[0]; - zipExportService.exportToZip(taskContext, branch, format, res); + zipExportService.exportToZip(taskContext, branch, format as "html" | "markdown", res); }); eu.route(router, 'post', '/etapi/notes/:noteId/import', (req, res, next) => { @@ -186,23 +192,24 @@ function register(router) { }); } -function parseSearchParams(req) { - const rawSearchParams = { +function parseSearchParams(req: AppRequest) { + const rawSearchParams: SearchParams = { fastSearch: parseBoolean(req.query, 'fastSearch'), includeArchivedNotes: parseBoolean(req.query, 'includeArchivedNotes'), - ancestorNoteId: req.query['ancestorNoteId'], - ancestorDepth: req.query['ancestorDepth'], // e.g. "eq5" - orderBy: req.query['orderBy'], - orderDirection: parseOrderDirection(req.query, 'orderDirection'), + ancestorNoteId: parseString(req.query['ancestorNoteId']), + ancestorDepth: parseString(req.query['ancestorDepth']), // e.g. "eq5" + orderBy: parseString(req.query['orderBy']), + // TODO: Check why the order direction was provided as a number, but it's a string everywhere else. + orderDirection: parseOrderDirection(req.query, 'orderDirection') as unknown as string, limit: parseInteger(req.query, 'limit'), debug: parseBoolean(req.query, 'debug') }; - const searchParams = {}; + const searchParams: SearchParams = {}; - for (const paramName of Object.keys(rawSearchParams)) { + for (const paramName of Object.keys(rawSearchParams) as (keyof SearchParams)[]) { if (rawSearchParams[paramName] !== undefined) { - searchParams[paramName] = rawSearchParams[paramName]; + (searchParams as any)[paramName] = rawSearchParams[paramName]; } } @@ -211,7 +218,15 @@ function parseSearchParams(req) { const SEARCH_PARAM_ERROR = "SEARCH_PARAM_VALIDATION_ERROR"; -function parseBoolean(obj, name) { +function parseString(value: string | ParsedQs | string[] | ParsedQs[] | undefined): string | undefined { + if (typeof value === "string") { + return value; + } + + return undefined; +} + +function parseBoolean(obj: any, name: string) { if (!(name in obj)) { return undefined; } @@ -223,11 +238,7 @@ function parseBoolean(obj, name) { return obj[name] === 'true'; } -function parseOrderDirection(obj, name) { - if (!(name in obj)) { - return undefined; - } - +function parseOrderDirection(obj: any, name: string) { const integer = parseInt(obj[name]); if (!['asc', 'desc'].includes(obj[name])) { @@ -237,7 +248,7 @@ function parseOrderDirection(obj, name) { return integer; } -function parseInteger(obj, name) { +function parseInteger(obj: any, name: string) { if (!(name in obj)) { return undefined; } @@ -251,6 +262,6 @@ function parseInteger(obj, name) { return integer; } -module.exports = { +export = { register }; diff --git a/src/routes/api/files.ts b/src/routes/api/files.ts index 44cd1dacf..682bc94e4 100644 --- a/src/routes/api/files.ts +++ b/src/routes/api/files.ts @@ -20,6 +20,13 @@ function updateFile(req: AppRequest) { const note = becca.getNoteOrThrow(req.params.noteId); const file = req.file; + if (!file) { + return { + uploaded: false, + message: `Missing file.` + }; + } + note.saveRevision(); note.mime = file.mimetype.toLowerCase(); @@ -39,6 +46,12 @@ function updateFile(req: AppRequest) { function updateAttachment(req: AppRequest) { const attachment = becca.getAttachmentOrThrow(req.params.attachmentId); const file = req.file; + if (!file) { + return { + uploaded: false, + message: `Missing file.` + }; + } attachment.getNote().saveRevision(); diff --git a/src/routes/api/image.ts b/src/routes/api/image.ts index a0ee6140c..b00c7fd7f 100644 --- a/src/routes/api/image.ts +++ b/src/routes/api/image.ts @@ -88,6 +88,13 @@ function updateImage(req: AppRequest) { const note = becca.getNoteOrThrow(noteId); + if (!file) { + return { + uploaded: false, + message: `Missing image data.` + }; + } + if (!["image/png", "image/jpeg", "image/gif", "image/webp", "image/svg+xml"].includes(file.mimetype)) { return { uploaded: false, diff --git a/src/routes/api/sender.ts b/src/routes/api/sender.ts index 515011739..8d20c1fd9 100644 --- a/src/routes/api/sender.ts +++ b/src/routes/api/sender.ts @@ -11,6 +11,13 @@ import { AppRequest } from '../route-interface'; function uploadImage(req: AppRequest) { const file = req.file; + if (!file) { + return { + uploaded: false, + message: `Missing image data.` + }; + } + if (!["image/png", "image/jpeg", "image/gif", "image/webp", "image/svg+xml"].includes(file.mimetype)) { return [400, `Unknown image type: ${file.mimetype}`]; } diff --git a/src/routes/login.ts b/src/routes/login.ts index a4c13ca90..cce214fbe 100644 --- a/src/routes/login.ts +++ b/src/routes/login.ts @@ -67,7 +67,7 @@ function login(req: AppRequest, res: Response) { if (rememberMe) { req.session.cookie.maxAge = 21 * 24 * 3600000; // 3 weeks } else { - req.session.cookie.expires = false; + req.session.cookie.expires = null; } req.session.loggedIn = true; diff --git a/src/routes/route-interface.ts b/src/routes/route-interface.ts index acf86cbe5..4510c5de6 100644 --- a/src/routes/route-interface.ts +++ b/src/routes/route-interface.ts @@ -1,5 +1,5 @@ -import { Request } from "express"; -import { File } from "../services/import/common"; +import { NextFunction, Request, Response } from "express"; +import { Session, SessionData } from "express-session"; export interface AppRequest extends Request { headers: { @@ -7,14 +7,15 @@ export interface AppRequest extends Request { "trilium-cred"?: string; "x-local-date"?: string; "x-labels"?: string; + "trilium-local-now-datetime"?: string; } - session: { - loggedIn: boolean; - cookie: { - maxAge: number; - expires: boolean - }; - regenerate: (callback: () => void) => void; + session: Session & Partial & { + loggedIn: boolean; } - file: File; -} \ No newline at end of file +} + +export type AppRequestHandler = ( + req: AppRequest, + res: Response, + next: NextFunction +) => void; \ No newline at end of file diff --git a/src/routes/routes.js b/src/routes/routes.js index efa8afb8c..dc330e54f 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -66,7 +66,7 @@ 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.js'); +const etapiNoteRoutes = require('../etapi/notes'); const etapiSpecialNoteRoutes = require('../etapi/special_notes'); const etapiSpecRoute = require('../etapi/spec.js'); const etapiBackupRoute = require('../etapi/backup');