diff --git a/src/becca/entities/bnote.ts b/src/becca/entities/bnote.ts index b0edd8c61..971055755 100644 --- a/src/becca/entities/bnote.ts +++ b/src/becca/entities/bnote.ts @@ -1618,7 +1618,7 @@ class BNote extends AbstractBeccaEntity { * @param matchBy - choose by which property we detect if to update an existing attachment. * Supported values are either 'attachmentId' (default) or 'title' */ - saveAttachment({ attachmentId, role, mime, title, content, position }: AttachmentRow, matchBy = "attachmentId") { + saveAttachment({ attachmentId, role, mime, title, content, position }: AttachmentRow, matchBy: "attachmentId" | "title" | undefined = "attachmentId") { if (!["attachmentId", "title"].includes(matchBy)) { throw new Error(`Unsupported value '${matchBy}' for matchBy param, has to be either 'attachmentId' or 'title'.`); } diff --git a/src/becca/entities/brevision.ts b/src/becca/entities/brevision.ts index 6167ad4b2..bc9ce360c 100644 --- a/src/becca/entities/brevision.ts +++ b/src/becca/entities/brevision.ts @@ -7,7 +7,7 @@ import becca from "../becca.js"; import AbstractBeccaEntity from "./abstract_becca_entity.js"; import sql from "../../services/sql.js"; import BAttachment from "./battachment.js"; -import type { AttachmentRow, RevisionRow } from "./rows.js"; +import type { AttachmentRow, NoteType, RevisionRow } from "./rows.js"; import eraseService from "../../services/erase.js"; interface ContentOpts { @@ -36,7 +36,7 @@ class BRevision extends AbstractBeccaEntity { revisionId?: string; noteId!: string; - type!: string; + type!: NoteType; mime!: string; title!: string; dateLastEdited?: string; diff --git a/src/becca/entities/rows.ts b/src/becca/entities/rows.ts index ae7fbd383..3730ed922 100644 --- a/src/becca/entities/rows.ts +++ b/src/becca/entities/rows.ts @@ -22,7 +22,7 @@ export interface AttachmentRow { export interface RevisionRow { revisionId?: string; noteId: string; - type: string; + type: NoteType; mime: string; isProtected?: boolean; title: string; diff --git a/src/errors/forbidden_error.ts b/src/errors/forbidden_error.ts new file mode 100644 index 000000000..3e62665b0 --- /dev/null +++ b/src/errors/forbidden_error.ts @@ -0,0 +1,12 @@ +import HttpError from "./http_error.js"; + +class ForbiddenError extends HttpError { + + constructor(message: string) { + super(message, 403); + this.name = "ForbiddenError"; + } + +} + +export default ForbiddenError; \ No newline at end of file diff --git a/src/errors/http_error.ts b/src/errors/http_error.ts new file mode 100644 index 000000000..2ab806d8b --- /dev/null +++ b/src/errors/http_error.ts @@ -0,0 +1,13 @@ +class HttpError extends Error { + + statusCode: number; + + constructor(message: string, statusCode: number) { + super(message); + this.name = "HttpError"; + this.statusCode = statusCode; + } + +} + +export default HttpError; \ No newline at end of file diff --git a/src/errors/not_found_error.ts b/src/errors/not_found_error.ts index 6d8fbe4d8..44f718a2c 100644 --- a/src/errors/not_found_error.ts +++ b/src/errors/not_found_error.ts @@ -1,9 +1,12 @@ -class NotFoundError { - message: string; +import HttpError from "./http_error.js"; + +class NotFoundError extends HttpError { constructor(message: string) { - this.message = message; + super(message, 404); + this.name = "NotFoundError"; } + } export default NotFoundError; diff --git a/src/errors/validation_error.ts b/src/errors/validation_error.ts index f9c0ba6fc..25cdd509e 100644 --- a/src/errors/validation_error.ts +++ b/src/errors/validation_error.ts @@ -1,9 +1,12 @@ -class ValidationError { - message: string; +import HttpError from "./http_error.js"; + +class ValidationError extends HttpError { constructor(message: string) { - this.message = message; + super(message, 400) + this.name = "ValidationError"; } + } export default ValidationError; diff --git a/src/public/translations/en/translation.json b/src/public/translations/en/translation.json index 62729a924..5d6b0221a 100644 --- a/src/public/translations/en/translation.json +++ b/src/public/translations/en/translation.json @@ -376,7 +376,7 @@ "auto_read_only_disabled": "text/code notes can be set automatically into read mode when they are too large. You can disable this behavior on per-note basis by adding this label to the note", "app_css": "marks CSS notes which are loaded into the Trilium application and can thus be used to modify Trilium's looks.", "app_theme": "marks CSS notes which are full Trilium themes and are thus available in Trilium options.", - "app_theme_base": "set to \"next\" in order to use the TriliumNext theme as a base for a custom theme instead of the legacy one.", + "app_theme_base": "set to \"next\", \"next-light\", or \"next-dark\" to use the corresponding TriliumNext theme (auto, light or dark) as the base for a custom theme, instead of the legacy one.", "css_class": "value of this label is then added as CSS class to the node representing given note in the tree. This can be useful for advanced theming. Can be used in template notes.", "icon_class": "value of this label is added as a CSS class to the icon on the tree which can help visually distinguish the notes in the tree. Example might be bx bx-home - icons are taken from boxicons. Can be used in template notes.", "page_size": "number of items per page in note listing", diff --git a/src/routes/api/attachments.ts b/src/routes/api/attachments.ts index a609b93dc..718c99914 100644 --- a/src/routes/api/attachments.ts +++ b/src/routes/api/attachments.ts @@ -33,7 +33,9 @@ function getAllAttachments(req: Request) { function saveAttachment(req: Request) { const { noteId } = req.params; const { attachmentId, role, mime, title, content } = req.body; - const { matchBy } = req.query as any; + const matchByQuery = req.query.matchBy + const isValidMatchBy = (typeof matchByQuery === "string") && (matchByQuery === "attachmentId" || matchByQuery === "title"); + const matchBy = isValidMatchBy ? matchByQuery : undefined; const note = becca.getNoteOrThrow(noteId); note.saveAttachment({ attachmentId, role, mime, title, content }, matchBy); @@ -41,7 +43,14 @@ function saveAttachment(req: Request) { function uploadAttachment(req: Request) { const { noteId } = req.params; - const { file } = req as any; + const { file } = req; + + if (!file) { + return { + uploaded: false, + message: `Missing attachment data.` + }; + } const note = becca.getNoteOrThrow(noteId); let url; diff --git a/src/routes/api/clipper.ts b/src/routes/api/clipper.ts index 4d821e480..9f1510fcb 100644 --- a/src/routes/api/clipper.ts +++ b/src/routes/api/clipper.ts @@ -30,12 +30,12 @@ function addClipping(req: Request) { // if a note under the clipperInbox has the same 'pageUrl' attribute, // add the content to that note and clone it under today's inbox // otherwise just create a new note under today's inbox - let { title, content, pageUrl, images } = req.body; + const { title, content, images } = req.body; const clipType = "clippings"; const clipperInbox = getClipperInboxNote(); - pageUrl = htmlSanitizer.sanitizeUrl(pageUrl); + const pageUrl = htmlSanitizer.sanitizeUrl(req.body.pageUrl); let clippingNote = findClippingNote(clipperInbox, pageUrl, clipType); if (!clippingNote) { @@ -100,16 +100,15 @@ function getClipperInboxNote() { } function createNote(req: Request) { - let { title, content, pageUrl, images, clipType, labels } = req.body; + const { content, images, labels } = req.body; - if (!title || !title.trim()) { - title = `Clipped note from ${pageUrl}`; - } + const clipType = htmlSanitizer.sanitize(req.body.clipType); + const pageUrl = htmlSanitizer.sanitizeUrl(req.body.pageUrl); - clipType = htmlSanitizer.sanitize(clipType); + const trimmedTitle = (typeof req.body.title === "string") ? req.body.title.trim() : ""; + const title = trimmedTitle || `Clipped note from ${pageUrl}`; const clipperInbox = getClipperInboxNote(); - pageUrl = htmlSanitizer.sanitizeUrl(pageUrl); let note = findClippingNote(clipperInbox, pageUrl, clipType); if (!note) { @@ -123,8 +122,6 @@ function createNote(req: Request) { note.setLabel("clipType", clipType); if (pageUrl) { - pageUrl = htmlSanitizer.sanitizeUrl(pageUrl); - note.setLabel("pageUrl", pageUrl); note.setLabel("iconClass", "bx bx-globe"); } @@ -139,7 +136,7 @@ function createNote(req: Request) { const existingContent = note.getContent(); if (typeof existingContent !== "string") { - throw new ValidationError("Invalid note content tpye."); + throw new ValidationError("Invalid note content type."); } const rewrittenContent = processContent(images, note, content); const newContent = `${existingContent}${existingContent.trim() ? "
" : ""}${rewrittenContent}`; @@ -219,9 +216,9 @@ function handshake() { } function findNotesByUrl(req: Request) { - let pageUrl = req.params.noteUrl; + const pageUrl = req.params.noteUrl; const clipperInbox = getClipperInboxNote(); - let foundPage = findClippingNote(clipperInbox, pageUrl, null); + const foundPage = findClippingNote(clipperInbox, pageUrl, null); return { noteId: foundPage ? foundPage.noteId : null }; diff --git a/src/routes/api/export.ts b/src/routes/api/export.ts index 9023125f9..7433cd552 100644 --- a/src/routes/api/export.ts +++ b/src/routes/api/export.ts @@ -9,6 +9,7 @@ import log from "../../services/log.js"; import NotFoundError from "../../errors/not_found_error.js"; import type { Request, Response } from "express"; import ValidationError from "../../errors/validation_error.js"; +import { safeExtractMessageAndStackFromError } from "../../services/utils.js"; function exportBranch(req: Request, res: Response) { const { branchId, type, format, version, taskId } = req.params; @@ -37,11 +38,12 @@ function exportBranch(req: Request, res: Response) { } else { throw new NotFoundError(`Unrecognized export format '${format}'`); } - } catch (e: any) { - const message = `Export failed with following error: '${e.message}'. More details might be in the logs.`; + } catch (e: unknown) { + const [errMessage, errStack] = safeExtractMessageAndStackFromError(e); + const message = `Export failed with following error: '${errMessage}'. More details might be in the logs.`; taskContext.reportError(message); - log.error(message + e.stack); + log.error(errMessage + errStack); res.setHeader("Content-Type", "text/plain").status(500).send(message); } diff --git a/src/routes/api/image.ts b/src/routes/api/image.ts index d2d943486..d2f2b5632 100644 --- a/src/routes/api/image.ts +++ b/src/routes/api/image.ts @@ -82,7 +82,7 @@ function updateImage(req: Request) { const { noteId } = req.params; const { file } = req; - const note = becca.getNoteOrThrow(noteId); + const _note = becca.getNoteOrThrow(noteId); if (!file) { return { diff --git a/src/routes/api/import.ts b/src/routes/api/import.ts index bb46f8990..6dfce5870 100644 --- a/src/routes/api/import.ts +++ b/src/routes/api/import.ts @@ -13,6 +13,7 @@ import TaskContext from "../../services/task_context.js"; import ValidationError from "../../errors/validation_error.js"; import type { Request } from "express"; import type BNote from "../../becca/entities/bnote.js"; +import { safeExtractMessageAndStackFromError } from "../../services/utils.js"; async function importNotesToBranch(req: Request) { const { parentNoteId } = req.params; @@ -68,11 +69,12 @@ async function importNotesToBranch(req: Request) { } else { note = await singleImportService.importSingleFile(taskContext, file, parentNote); } - } catch (e: any) { - const message = `Import failed with following error: '${e.message}'. More details might be in the logs.`; + } catch (e: unknown) { + const [errMessage, errStack] = safeExtractMessageAndStackFromError(e); + const message = `Import failed with following error: '${errMessage}'. More details might be in the logs.`; taskContext.reportError(message); - log.error(message + e.stack); + log.error(message + errStack); return [500, message]; } @@ -120,11 +122,13 @@ async function importAttachmentsToNote(req: Request) { try { await singleImportService.importAttachment(taskContext, file, parentNote); - } catch (e: any) { - const message = `Import failed with following error: '${e.message}'. More details might be in the logs.`; + } catch (e: unknown) { + const [errMessage, errStack] = safeExtractMessageAndStackFromError(e); + + const message = `Import failed with following error: '${errMessage}'. More details might be in the logs.`; taskContext.reportError(message); - log.error(message + e.stack); + log.error(message + errStack); return [500, message]; } diff --git a/src/routes/api/recent_changes.ts b/src/routes/api/recent_changes.ts index 55a5ee9da..67b7436a0 100644 --- a/src/routes/api/recent_changes.ts +++ b/src/routes/api/recent_changes.ts @@ -5,7 +5,6 @@ import protectedSessionService from "../../services/protected_session.js"; import noteService from "../../services/notes.js"; import becca from "../../becca/becca.js"; import type { Request } from "express"; -import type { RevisionRow } from "../../becca/entities/rows.js"; interface RecentChangeRow { noteId: string; diff --git a/src/routes/api/relation-map.ts b/src/routes/api/relation-map.ts index 503bb36ef..2cf591b50 100644 --- a/src/routes/api/relation-map.ts +++ b/src/routes/api/relation-map.ts @@ -30,7 +30,7 @@ function getRelationMap(req: Request) { return resp; } - const questionMarks = noteIds.map((noteId) => "?").join(","); + const questionMarks = noteIds.map((_noteId) => "?").join(","); const relationMapNote = becca.getNoteOrThrow(relationMapNoteId); diff --git a/src/routes/api/revisions.ts b/src/routes/api/revisions.ts index e65d20113..18fbb7c39 100644 --- a/src/routes/api/revisions.ts +++ b/src/routes/api/revisions.ts @@ -1,7 +1,6 @@ "use strict"; import beccaService from "../../becca/becca_service.js"; -import revisionService from "../../services/revisions.js"; import utils from "../../services/utils.js"; import sql from "../../services/sql.js"; import cls from "../../services/cls.js"; @@ -111,7 +110,7 @@ function eraseRevision(req: Request) { } function eraseAllExcessRevisions() { - let allNoteIds = sql.getRows("SELECT noteId FROM notes WHERE SUBSTRING(noteId, 1, 1) != '_'") as { noteId: string }[]; + const allNoteIds = sql.getRows("SELECT noteId FROM notes WHERE SUBSTRING(noteId, 1, 1) != '_'") as { noteId: string }[]; allNoteIds.forEach((row) => { becca.getNote(row.noteId)?.eraseExcessRevisionSnapshots(); }); @@ -145,7 +144,7 @@ function restoreRevision(req: Request) { note.title = revision.title; note.mime = revision.mime; - note.type = revision.type as any; + note.type = revision.type; note.setContent(revisionContent, { forceSave: true }); }); } diff --git a/src/routes/api/script.ts b/src/routes/api/script.ts index 6907a5d9c..c702a82d8 100644 --- a/src/routes/api/script.ts +++ b/src/routes/api/script.ts @@ -6,6 +6,7 @@ import becca from "../../becca/becca.js"; import syncService from "../../services/sync.js"; import sql from "../../services/sql.js"; import type { Request } from "express"; +import { safeExtractMessageAndStackFromError } from "../../services/utils.js"; interface ScriptBody { script: string; @@ -33,8 +34,12 @@ async function exec(req: Request) { executionResult: result, maxEntityChangeId: syncService.getMaxEntityChangeId() }; - } catch (e: any) { - return { success: false, error: e.message }; + } catch (e: unknown) { + const [errMessage] = safeExtractMessageAndStackFromError(e); + return { + success: false, + error: errMessage + }; } } diff --git a/src/routes/api/similar_notes.ts b/src/routes/api/similar_notes.ts index 930f53282..8cd82dc72 100644 --- a/src/routes/api/similar_notes.ts +++ b/src/routes/api/similar_notes.ts @@ -8,7 +8,7 @@ import becca from "../../becca/becca.js"; async function getSimilarNotes(req: Request) { const noteId = req.params.noteId; - const note = becca.getNoteOrThrow(noteId); + const _note = becca.getNoteOrThrow(noteId); return await similarityService.findSimilarNotes(noteId); } diff --git a/src/routes/api/sql.ts b/src/routes/api/sql.ts index ed440f42d..a78c3e2f4 100644 --- a/src/routes/api/sql.ts +++ b/src/routes/api/sql.ts @@ -4,6 +4,7 @@ import sql from "../../services/sql.js"; import becca from "../../becca/becca.js"; import type { Request } from "express"; import ValidationError from "../../errors/validation_error.js"; +import { safeExtractMessageAndStackFromError } from "../../services/utils.js"; function getSchema() { const tableNames = sql.getColumn(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name`); @@ -56,10 +57,11 @@ function execute(req: Request) { success: true, results }; - } catch (e: any) { + } catch (e: unknown) { + const [errMessage] = safeExtractMessageAndStackFromError(e); return { success: false, - error: e.message + error: errMessage }; } } diff --git a/src/routes/api/sync.ts b/src/routes/api/sync.ts index 736e1e97b..50088a097 100644 --- a/src/routes/api/sync.ts +++ b/src/routes/api/sync.ts @@ -9,7 +9,7 @@ import optionService from "../../services/options.js"; import contentHashService from "../../services/content_hash.js"; import log from "../../services/log.js"; import syncOptions from "../../services/sync_options.js"; -import utils from "../../services/utils.js"; +import utils, { safeExtractMessageAndStackFromError } from "../../services/utils.js"; import ws from "../../services/ws.js"; import type { Request } from "express"; import type { EntityChange } from "../../services/entity_changes_interface.js"; @@ -30,10 +30,11 @@ async function testSync() { syncService.sync(); return { success: true, message: t("test_sync.successful") }; - } catch (e: any) { + } catch (e: unknown) { + const [errMessage] = safeExtractMessageAndStackFromError(e); return { success: false, - message: e.message + error: errMessage }; } } diff --git a/src/routes/api_docs.ts b/src/routes/api_docs.ts index 10d894056..df069a24f 100644 --- a/src/routes/api_docs.ts +++ b/src/routes/api_docs.ts @@ -1,4 +1,4 @@ -import type { Application, Router } from "express"; +import type { Application } from "express"; import swaggerUi from "swagger-ui-express"; import { readFile } from "fs/promises"; import { fileURLToPath } from "url"; diff --git a/src/routes/assets.ts b/src/routes/assets.ts index 37585d8d7..a26c22685 100644 --- a/src/routes/assets.ts +++ b/src/routes/assets.ts @@ -5,7 +5,7 @@ import express from "express"; import { isDev, isElectron } from "../services/utils.js"; import type serveStatic from "serve-static"; -const persistentCacheStatic = (root: string, options?: serveStatic.ServeStaticOptions>>) => { +const persistentCacheStatic = (root: string, options?: serveStatic.ServeStaticOptions>>) => { if (!isDev) { options = { maxAge: "1y", diff --git a/src/routes/custom.ts b/src/routes/custom.ts index f2b961f80..093a0e6eb 100644 --- a/src/routes/custom.ts +++ b/src/routes/custom.ts @@ -5,6 +5,7 @@ import cls from "../services/cls.js"; import sql from "../services/sql.js"; import becca from "../becca/becca.js"; import type { Request, Response, Router } from "express"; +import { safeExtractMessageAndStackFromError } from "../services/utils.js"; function handleRequest(req: Request, res: Response) { // express puts content after first slash into 0 index element @@ -25,8 +26,9 @@ function handleRequest(req: Request, res: Response) { try { match = path.match(regex); - } catch (e: any) { - log.error(`Testing path for label '${attr.attributeId}', regex '${attr.value}' failed with error: ${e.message}, stack: ${e.stack}`); + } catch (e: unknown) { + const [errMessage, errStack] = safeExtractMessageAndStackFromError(e); + log.error(`Testing path for label '${attr.attributeId}', regex '${attr.value}' failed with error: ${errMessage}, stack: ${errStack}`); continue; } @@ -45,10 +47,10 @@ function handleRequest(req: Request, res: Response) { req, res }); - } catch (e: any) { - log.error(`Custom handler '${note.noteId}' failed with: ${e.message}, ${e.stack}`); - - res.setHeader("Content-Type", "text/plain").status(500).send(e.message); + } catch (e: unknown) { + const [errMessage, errStack] = safeExtractMessageAndStackFromError(e); + log.error(`Custom handler '${note.noteId}' failed with: ${errMessage}, ${errStack}`); + res.setHeader("Content-Type", "text/plain").status(500).send(errMessage); } } else if (attr.name === "customResourceProvider") { fileService.downloadNoteInt(attr.noteId, res); @@ -68,7 +70,7 @@ function handleRequest(req: Request, res: Response) { function register(router: Router) { // explicitly no CSRF middleware since it's meant to allow integration from external services - router.all("/custom/:path*", (req: Request, res: Response, next) => { + router.all("/custom/:path*", (req: Request, res: Response, _next) => { cls.namespace.bindEmitter(req); cls.namespace.bindEmitter(res); diff --git a/src/routes/electron.ts b/src/routes/electron.ts index 13147a803..05e21e77b 100644 --- a/src/routes/electron.ts +++ b/src/routes/electron.ts @@ -7,7 +7,7 @@ interface Response { setHeader: (name: string, value: string) => Response; header: (name: string, value: string) => Response; status: (statusCode: number) => Response; - send: (obj: {}) => void; + send: (obj: {}) => void; // eslint-disable-line @typescript-eslint/no-empty-object-type } function init(app: Application) { diff --git a/src/routes/error_handlers.ts b/src/routes/error_handlers.ts index a73599e38..05b05f6a4 100644 --- a/src/routes/error_handlers.ts +++ b/src/routes/error_handlers.ts @@ -1,38 +1,46 @@ import type { Application, NextFunction, Request, Response } from "express"; import log from "../services/log.js"; +import NotFoundError from "../errors/not_found_error.js"; +import ForbiddenError from "../errors/forbidden_error.js"; +import HttpError from "../errors/http_error.js"; function register(app: Application) { - app.use((err: any, req: Request, res: Response, next: NextFunction) => { - if (err.code !== "EBADCSRFTOKEN") { - return next(err); + + app.use((err: unknown | Error, req: Request, res: Response, next: NextFunction) => { + + const isCsrfTokenError = typeof err === "object" + && err + && "code" in err + && err.code === "EBADCSRFTOKEN"; + + if (isCsrfTokenError) { + log.error(`Invalid CSRF token: ${req.headers["x-csrf-token"]}, secret: ${req.cookies["_csrf"]}`); + return next(new ForbiddenError("Invalid CSRF token")); } - log.error(`Invalid CSRF token: ${req.headers["x-csrf-token"]}, secret: ${req.cookies["_csrf"]}`); - - err = new Error("Invalid CSRF token"); - err.status = 403; - next(err); + return next(err); }); // catch 404 and forward to error handler app.use((req, res, next) => { - const err = new Error(`Router not found for request ${req.method} ${req.url}`); - (err as any).status = 404; + const err = new NotFoundError(`Router not found for request ${req.method} ${req.url}`); next(err); }); // error handler - app.use((err: any, req: Request, res: Response, next: NextFunction) => { - if (err.status !== 404) { - log.info(err); - } else { - log.info(`${err.status} ${req.method} ${req.url}`); - } + app.use((err: unknown | Error, req: Request, res: Response, _next: NextFunction) => { - res.status(err.status || 500); - res.send({ - message: err.message + const statusCode = (err instanceof HttpError) ? err.statusCode : 500; + const errMessage = (err instanceof Error && statusCode !== 404) + ? err + : `${statusCode} ${req.method} ${req.url}`; + + log.info(errMessage); + + res.status(statusCode).send({ + message: err instanceof Error ? err.message : "Unknown Error" }); + }); } diff --git a/src/routes/index.ts b/src/routes/index.ts index 966f71ab3..e7cd36228 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -37,7 +37,7 @@ function index(req: Request, res: Response) { device: view, csrfToken: csrfToken, themeCssUrl: getThemeCssUrl(theme, themeNote), - themeUseNextAsBase: themeNote?.getAttributeValue("label", "appThemeBase") === "next", + themeUseNextAsBase: themeNote?.getAttributeValue("label", "appThemeBase"), headingStyle: options.headingStyle, layoutOrientation: options.layoutOrientation, platform: process.platform, diff --git a/src/routes/routes.ts b/src/routes/routes.ts index 05c7612f2..de2055d99 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -1,6 +1,6 @@ "use strict"; -import { isElectron } from "../services/utils.js"; +import { isElectron, safeExtractMessageAndStackFromError } from "../services/utils.js"; import multer from "multer"; import log from "../services/log.js"; import express from "express"; @@ -471,7 +471,7 @@ function route(method: HttpMethod, path: string, middleware: express.Handler[], if (result?.then) { // promise - result.then((promiseResult: unknown) => handleResponse(resultHandler, req, res, promiseResult, start)).catch((e: any) => handleException(e, method, path, res)); + result.then((promiseResult: unknown) => handleResponse(resultHandler, req, res, promiseResult, start)).catch((e: unknown) => handleException(e, method, path, res)); } else { handleResponse(resultHandler, req, res, result, start); } @@ -487,22 +487,17 @@ function handleResponse(resultHandler: ApiResultHandler, req: express.Request, r log.request(req, res, Date.now() - start, responseLength); } -function handleException(e: any, method: HttpMethod, path: string, res: express.Response) { - log.error(`${method} ${path} threw exception: '${e.message}', stack: ${e.stack}`); +function handleException(e: unknown | Error, method: HttpMethod, path: string, res: express.Response) { + const [errMessage, errStack] = safeExtractMessageAndStackFromError(e); + + log.error(`${method} ${path} threw exception: '${errMessage}', stack: ${errStack}`); + + const resStatusCode = (e instanceof ValidationError || e instanceof NotFoundError) ? e.statusCode : 500; + + res.status(resStatusCode).json({ + message: errMessage + }); - 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).json({ - message: e.message - }); - } } function createUploadMiddleware() { diff --git a/src/services/utils.spec.ts b/src/services/utils.spec.ts index 52c173da4..afa1ba4e7 100644 --- a/src/services/utils.spec.ts +++ b/src/services/utils.spec.ts @@ -500,6 +500,23 @@ describe("#isDev", () => { }); }); +describe("#safeExtractMessageAndStackFromError", () => { + it("should correctly extract the message and stack property if it gets passed an instance of an Error", () => { + const testMessage = "Test Message"; + const testError = new Error(testMessage); + const actual = utils.safeExtractMessageAndStackFromError(testError); + expect(actual[0]).toBe(testMessage); + expect(actual[1]).not.toBeUndefined(); + }); + + it("should use the fallback 'Unknown Error' message, if it gets passed anything else than an instance of an Error", () => { + const testNonError = "this is not an instance of an Error, but JS technically allows us to throw this anyways"; + const actual = utils.safeExtractMessageAndStackFromError(testNonError); + expect(actual[0]).toBe("Unknown Error"); + expect(actual[1]).toBeUndefined(); + }); +}) + describe("#formatDownloadTitle", () => { //prettier-ignore const testCases: [fnValue: Parameters, expectedValue: ReturnType][] = [ diff --git a/src/services/utils.ts b/src/services/utils.ts index ff1af664e..966e14840 100644 --- a/src/services/utils.ts +++ b/src/services/utils.ts @@ -362,6 +362,11 @@ export function processStringOrBuffer(data: string | Buffer | null) { } } +export function safeExtractMessageAndStackFromError(err: unknown) { + return (err instanceof Error) ? [err.message, err.stack] as const : ["Unknown Error", undefined] as const; +} + + export default { compareVersions, crash, @@ -392,6 +397,7 @@ export default { removeDiacritic, removeTextFileExtension, replaceAll, + safeExtractMessageAndStackFromError, sanitizeSqlIdentifier, stripTags, timeLimit, diff --git a/src/views/desktop.ejs b/src/views/desktop.ejs index 6896eeba8..40f369c85 100644 --- a/src/views/desktop.ejs +++ b/src/views/desktop.ejs @@ -53,8 +53,12 @@ <% } %> -<% if (themeUseNextAsBase) { %> - +<% if (themeUseNextAsBase === "next") { %> + +<% } else if (themeUseNextAsBase === "next-dark") { %> + +<% } else if (themeUseNextAsBase === "next-light") { %> + <% } %>