Notes/apps/server/src/etapi/etapi_utils.ts

157 lines
5.0 KiB
TypeScript
Raw Normal View History

import cls from "../services/cls.js";
import sql from "../services/sql.js";
import log from "../services/log.js";
import becca from "../becca/becca.js";
import etapiTokenService from "../services/etapi_tokens.js";
import config from "../services/config.js";
import type { NextFunction, Request, RequestHandler, Response, Router } from "express";
import type { ValidatorMap } from "./etapi-interface.js";
2025-05-21 15:42:53 +03:00
import type { ApiRequestHandler } from "../routes/route_api.js";
2022-01-07 19:33:59 +01:00
const GENERIC_CODE = "GENERIC";
2024-04-07 14:54:01 +03:00
type HttpMethod = "all" | "get" | "post" | "put" | "delete" | "patch" | "options" | "head";
2022-01-10 17:09:20 +01:00
const noAuthentication = config.General && config.General.noAuthentication === true;
2022-01-07 19:33:59 +01:00
class EtapiError extends Error {
2024-04-07 14:54:01 +03:00
statusCode: number;
code: string;
constructor(statusCode: number, code: string, message: string) {
2024-05-05 12:40:00 +02:00
super(message);
// Set the prototype explicitly.
Object.setPrototypeOf(this, EtapiError.prototype);
2022-01-07 19:33:59 +01:00
this.statusCode = statusCode;
this.code = code;
}
}
2024-04-07 14:54:01 +03:00
function sendError(res: Response, statusCode: number, code: string, message: string) {
2022-01-07 19:33:59 +01:00
return res
2025-01-09 18:07:02 +02:00
.set("Content-Type", "application/json")
2022-01-07 19:33:59 +01:00
.status(statusCode)
2025-01-09 18:07:02 +02:00
.send(
JSON.stringify({
status: statusCode,
code: code,
message: message
})
);
2022-01-07 19:33:59 +01:00
}
2024-04-07 14:54:01 +03:00
function checkEtapiAuth(req: Request, res: Response, next: NextFunction) {
2022-01-10 17:09:20 +01:00
if (noAuthentication || etapiTokenService.isValidAuthHeader(req.headers.authorization)) {
next();
2025-01-09 18:07:02 +02:00
} else {
2022-01-10 17:09:20 +01:00
sendError(res, 401, "NOT_AUTHENTICATED", "Not authenticated");
2022-01-07 19:33:59 +01:00
}
}
function processRequest(req: Request, res: Response, routeHandler: ApiRequestHandler, next: NextFunction, method: string, path: string) {
2022-01-10 17:09:20 +01:00
try {
cls.namespace.bindEmitter(req);
cls.namespace.bindEmitter(res);
2022-01-07 19:33:59 +01:00
2022-01-10 17:09:20 +01:00
cls.init(() => {
2025-01-09 18:07:02 +02:00
cls.set("componentId", "etapi");
cls.set("localNowDateTime", req.headers["trilium-local-now-datetime"]);
2022-01-07 19:33:59 +01:00
const cb = () => routeHandler(req, res, next);
2022-01-07 19:33:59 +01:00
2022-01-10 17:09:20 +01:00
return sql.transactional(cb);
});
2024-04-07 14:54:01 +03:00
} catch (e: any) {
2022-01-10 17:09:20 +01:00
log.error(`${method} ${path} threw exception ${e.message} with stacktrace: ${e.stack}`);
if (e instanceof EtapiError) {
sendError(res, e.statusCode, e.code, e.message);
} else {
sendError(res, 500, GENERIC_CODE, e.message);
2022-01-07 19:33:59 +01:00
}
2022-01-10 17:09:20 +01:00
}
}
function route(router: Router, method: HttpMethod, path: string, routeHandler: ApiRequestHandler) {
2024-04-07 14:54:01 +03:00
router[method](path, checkEtapiAuth, (req: Request, res: Response, next: NextFunction) => processRequest(req, res, routeHandler, next, method, path));
2022-01-10 17:09:20 +01:00
}
2024-04-07 14:54:01 +03:00
function NOT_AUTHENTICATED_ROUTE(router: Router, method: HttpMethod, path: string, middleware: RequestHandler[], routeHandler: RequestHandler) {
router[method](path, ...middleware, (req: Request, res: Response, next: NextFunction) => processRequest(req, res, routeHandler, next, method, path));
2022-01-07 19:33:59 +01:00
}
2024-04-07 14:54:01 +03:00
function getAndCheckNote(noteId: string) {
2022-01-07 19:33:59 +01:00
const note = becca.getNote(noteId);
2022-01-07 19:33:59 +01:00
if (note) {
return note;
2025-01-09 18:07:02 +02:00
} else {
2023-06-05 09:23:42 +02:00
throw new EtapiError(404, "NOTE_NOT_FOUND", `Note '${noteId}' not found.`);
}
}
2024-04-07 14:54:01 +03:00
function getAndCheckAttachment(attachmentId: string) {
2025-01-09 18:07:02 +02:00
const attachment = becca.getAttachment(attachmentId, { includeContentLength: true });
2023-06-05 09:23:42 +02:00
if (attachment) {
return attachment;
2025-01-09 18:07:02 +02:00
} else {
2023-06-05 09:23:42 +02:00
throw new EtapiError(404, "ATTACHMENT_NOT_FOUND", `Attachment '${attachmentId}' not found.`);
2022-01-07 19:33:59 +01:00
}
}
2024-04-07 14:54:01 +03:00
function getAndCheckBranch(branchId: string) {
2022-01-07 19:33:59 +01:00
const branch = becca.getBranch(branchId);
if (branch) {
return branch;
2025-01-09 18:07:02 +02:00
} else {
2023-06-05 09:23:42 +02:00
throw new EtapiError(404, "BRANCH_NOT_FOUND", `Branch '${branchId}' not found.`);
2022-01-07 19:33:59 +01:00
}
}
2024-04-07 14:54:01 +03:00
function getAndCheckAttribute(attributeId: string) {
2022-01-07 19:33:59 +01:00
const attribute = becca.getAttribute(attributeId);
if (attribute) {
return attribute;
2025-01-09 18:07:02 +02:00
} else {
2023-06-05 09:23:42 +02:00
throw new EtapiError(404, "ATTRIBUTE_NOT_FOUND", `Attribute '${attributeId}' not found.`);
2022-01-07 19:33:59 +01:00
}
}
2024-04-07 15:13:34 +03:00
function validateAndPatch(target: any, source: any, allowedProperties: ValidatorMap) {
2022-01-12 19:32:23 +01:00
for (const key of Object.keys(source)) {
2022-01-07 19:33:59 +01:00
if (!(key in allowedProperties)) {
2022-07-10 22:09:13 +02:00
throw new EtapiError(400, "PROPERTY_NOT_ALLOWED", `Property '${key}' is not allowed for this method.`);
2025-01-09 18:07:02 +02:00
} else {
2022-01-12 19:32:23 +01:00
for (const validator of allowedProperties[key]) {
const validationResult = validator(source[key]);
if (validationResult) {
2023-06-05 09:23:42 +02:00
throw new EtapiError(400, "PROPERTY_VALIDATION_ERROR", `Validation failed on property '${key}': ${validationResult}.`);
2022-01-12 19:32:23 +01:00
}
2022-01-07 19:33:59 +01:00
}
}
}
2022-01-07 19:33:59 +01:00
// validation passed, let's patch
2022-01-12 19:32:23 +01:00
for (const propName of Object.keys(source)) {
target[propName] = source[propName];
2022-01-07 19:33:59 +01:00
}
}
export default {
2022-01-07 19:33:59 +01:00
EtapiError,
sendError,
route,
2022-01-10 17:09:20 +01:00
NOT_AUTHENTICATED_ROUTE,
2022-01-07 19:33:59 +01:00
GENERIC_CODE,
validateAndPatch,
getAndCheckNote,
getAndCheckBranch,
2023-06-05 09:23:42 +02:00
getAndCheckAttribute,
getAndCheckAttachment
2025-01-09 18:07:02 +02:00
};