From 6f3ab274bd51ddf3a3ea486ec19a4698e6ac780a Mon Sep 17 00:00:00 2001 From: Alex Christoffer Rasmussen Date: Thu, 12 Jun 2025 15:02:35 +0200 Subject: [PATCH] Upgrade expressjs (#1633) * upgrade expressjs * migration * reenable test * extend timeout on download test * fixup! migration * move empty body test from controller to validator * enusre not empty * extract validation function * fixup! reenable test * remove thin controllers * disable e2e test of project file download --- apps/server/package.json | 22 +- .../src/api-data/assets/assets.controller.ts | 41 -- .../src/api-data/assets/assets.router.ts | 40 +- .../src/api-data/assets/assets.validation.ts | 14 +- .../automation/automation.controller.ts | 2 +- .../api-data/automation/automation.router.ts | 6 +- .../automation/automation.validation.ts | 62 +-- .../custom-fields/customFields.router.ts | 2 +- .../custom-fields/customFields.validation.ts | 26 +- apps/server/src/api-data/db/db.controller.ts | 24 +- apps/server/src/api-data/db/db.validation.ts | 59 +-- .../src/api-data/excel/excel.controller.ts | 45 -- .../server/src/api-data/excel/excel.router.ts | 44 +- .../src/api-data/excel/excel.validation.ts | 22 +- apps/server/src/api-data/index.ts | 4 +- .../project-data/projectData.controller.ts | 40 -- .../project-data/projectData.router.ts | 36 +- .../project-data/projectData.validation.ts | 13 +- .../src/api-data/report/report.controller.ts | 18 - .../src/api-data/report/report.router.ts | 21 +- .../src/api-data/rundown/rundown.router.ts | 18 +- .../api-data/rundown/rundown.validation.ts | 54 +-- .../api-data/session/session.controller.ts | 41 -- .../src/api-data/session/session.router.ts | 43 +- .../api-data/session/session.validation.ts | 11 +- .../api-data/settings/settings.controller.ts | 10 +- .../api-data/settings/settings.validation.ts | 20 +- .../src/api-data/sheets/sheets.validation.ts | 27 +- .../url-presets/urlPresets.controller.ts | 30 -- .../api-data/url-presets/urlPresets.router.ts | 26 +- .../url-presets/urlPresets.validation.ts | 16 +- .../validation-utils/validationFunction.ts | 34 ++ .../view-settings/viewSettings.validation.ts | 22 +- .../src/api-integration/integration.router.ts | 7 +- apps/server/src/app.ts | 4 +- apps/server/src/utils/routerUtils.ts | 39 -- e2e/tests/000-upload-showfile.spec.ts | 25 +- e2e/tests/fixtures/e2e-test-db.json | 25 +- pnpm-lock.yaml | 438 ++++++++---------- 39 files changed, 590 insertions(+), 841 deletions(-) delete mode 100644 apps/server/src/api-data/assets/assets.controller.ts delete mode 100644 apps/server/src/api-data/excel/excel.controller.ts delete mode 100644 apps/server/src/api-data/project-data/projectData.controller.ts delete mode 100644 apps/server/src/api-data/report/report.controller.ts delete mode 100644 apps/server/src/api-data/session/session.controller.ts delete mode 100644 apps/server/src/api-data/url-presets/urlPresets.controller.ts create mode 100644 apps/server/src/api-data/validation-utils/validationFunction.ts delete mode 100644 apps/server/src/utils/routerUtils.ts diff --git a/apps/server/package.json b/apps/server/package.json index 6f50b0606..92853557f 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -6,18 +6,18 @@ "exports": "./src/index.js", "dependencies": { "@googleapis/sheets": "^5.0.5", - "cookie": "^1.0.2", - "cookie-parser": "^1.4.7", - "cors": "^2.8.5", + "cookie": "1.0.2", + "cookie-parser": "1.4.7", + "cors": "2.8.5", "dotenv": "^16.0.1", - "express": "^4.21.1", - "express-static-gzip": "^2.2.0", - "express-validator": "^7.2.0", + "express": "5.1.0", + "express-static-gzip": "3.0.0", + "express-validator": "7.2.1", + "multer": "2.0.1", "fast-equals": "^5.0.1", "google-auth-library": "^9.4.2", "got": "^14.4.5", "lowdb": "^7.0.1", - "multer": "^1.4.5-lts.1", "ontime-utils": "workspace:*", "osc-min": "2.1.2", "sanitize-filename": "^1.6.3", @@ -26,10 +26,10 @@ "xlsx": "^0.18.5" }, "devDependencies": { - "@types/cookie-parser": "^1.4.8", - "@types/cors": "^2.8.17", - "@types/express": "^4.17.17", - "@types/multer": "^1.4.11", + "@types/cookie-parser": "1.4.9", + "@types/cors": "2.8.19", + "@types/express": "5.0.3", + "@types/multer": "1.4.13", "@types/node": "catalog:", "@types/websocket": "^1.0.5", "@types/ws": "^8.5.10", diff --git a/apps/server/src/api-data/assets/assets.controller.ts b/apps/server/src/api-data/assets/assets.controller.ts deleted file mode 100644 index 90b46244a..000000000 --- a/apps/server/src/api-data/assets/assets.controller.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { defaultCss } from '../../user/styles/bundledCss.js'; -import type { Request, Response } from 'express'; -import { readCssFile, writeCssFile } from './assets.service.js'; - -/** - * Exposes the contents of the cssOverride.css file - */ -export async function getCssOverride(_req: Request, res: Response) { - try { - const data = await readCssFile(); - res.status(200).send(data); - } catch (error) { - res.status(500).send({ message: error }); - } -} - -/** - * Allows modifying the cssOverride.css file - */ -export async function postCssOverride(req: Request, res: Response) { - const { css } = req.body; - - try { - await writeCssFile(css); - res.status(204).send(); - } catch (error) { - res.status(500).send({ message: error }); - } -} - -/** - * Restores the default cssOverride.css file - */ -export async function restoreCss(_req: Request, res: Response) { - try { - await writeCssFile(defaultCss); - res.status(200).send(defaultCss); - } catch (error) { - res.status(500).send({ message: error }); - } -} diff --git a/apps/server/src/api-data/assets/assets.router.ts b/apps/server/src/api-data/assets/assets.router.ts index 933972b38..606e4a9a7 100644 --- a/apps/server/src/api-data/assets/assets.router.ts +++ b/apps/server/src/api-data/assets/assets.router.ts @@ -1,10 +1,40 @@ import express from 'express'; - -import { getCssOverride, postCssOverride, restoreCss } from './assets.controller.js'; +import type { Request, Response } from 'express'; +import type { ErrorResponse } from 'ontime-types'; import { validatePostCss } from './assets.validation.js'; +import { readCssFile, writeCssFile } from './assets.service.js'; +import { getErrorMessage } from 'ontime-utils'; +import { defaultCss } from '../../user/styles/bundledCss.js'; export const router = express.Router(); -router.get('/css', getCssOverride); -router.post('/css', validatePostCss, postCssOverride); -router.post('/css/restore', restoreCss); +router.get('/css', async (_req: Request, res: Response) => { + try { + const data = await readCssFile(); + res.status(200).send(data); + } catch (error) { + const message = getErrorMessage(error); + res.status(500).send({ message }); + } +}); + +router.post('/css', validatePostCss, async (req: Request, res: Response) => { + const { css } = req.body; + try { + await writeCssFile(css); + res.status(204).send(); + } catch (error) { + const message = getErrorMessage(error); + res.status(500).send({ message }); + } +}); + +router.post('/css/restore', async (_req: Request, res: Response) => { + try { + await writeCssFile(defaultCss); + res.status(200).send(defaultCss); + } catch (error) { + const message = getErrorMessage(error); + res.status(500).send({ message }); + } +}); diff --git a/apps/server/src/api-data/assets/assets.validation.ts b/apps/server/src/api-data/assets/assets.validation.ts index 8f2ac8b73..10d134b38 100644 --- a/apps/server/src/api-data/assets/assets.validation.ts +++ b/apps/server/src/api-data/assets/assets.validation.ts @@ -1,12 +1,4 @@ -import { Request, Response, NextFunction } from 'express'; -import { body, validationResult } from 'express-validator'; +import { body } from 'express-validator'; +import { requestValidationFunction } from '../validation-utils/validationFunction.js'; -export const validatePostCss = [ - body('css').isString().trim(), - - (req: Request, res: Response, next: NextFunction) => { - const errors = validationResult(req); - if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); - next(); - }, -]; +export const validatePostCss = [body('css').isString().trim(), requestValidationFunction]; diff --git a/apps/server/src/api-data/automation/automation.controller.ts b/apps/server/src/api-data/automation/automation.controller.ts index b7790ffe5..cc797dee7 100644 --- a/apps/server/src/api-data/automation/automation.controller.ts +++ b/apps/server/src/api-data/automation/automation.controller.ts @@ -12,7 +12,7 @@ import * as automationService from './automation.service.js'; import { parseOutput } from './automation.validation.js'; export function getAutomationSettings(_req: Request, res: Response) { - res.json(automationDao.getAutomationSettings()); + res.status(200).json(automationDao.getAutomationSettings()); } export async function postAutomationSettings(req: Request, res: Response) { diff --git a/apps/server/src/api-data/automation/automation.router.ts b/apps/server/src/api-data/automation/automation.router.ts index 0a6fb0234..abd87d964 100644 --- a/apps/server/src/api-data/automation/automation.router.ts +++ b/apps/server/src/api-data/automation/automation.router.ts @@ -12,7 +12,6 @@ import { testOutput, } from './automation.controller.js'; import { - paramContainsId, validateAutomationSettings, validateAutomation, validateAutomationPatch, @@ -20,6 +19,7 @@ import { validateTrigger, validateTriggerPatch, } from './automation.validation.js'; +import { paramsWithId } from '../validation-utils/validationFunction.js'; export const router = express.Router(); @@ -28,10 +28,10 @@ router.post('/', validateAutomationSettings, postAutomationSettings); router.post('/trigger', validateTrigger, postTrigger); router.put('/trigger/:id', validateTriggerPatch, putTrigger); -router.delete('/trigger/:id', paramContainsId, deleteTrigger); +router.delete('/trigger/:id', paramsWithId, deleteTrigger); router.post('/automation', validateAutomation, postAutomation); router.put('/automation/:id', validateAutomationPatch, editAutomation); -router.delete('/automation/:id', paramContainsId, deleteAutomation); +router.delete('/automation/:id', paramsWithId, deleteAutomation); router.post('/test', validateTestPayload, testOutput); diff --git a/apps/server/src/api-data/automation/automation.validation.ts b/apps/server/src/api-data/automation/automation.validation.ts index 802c19575..6cecfb0b3 100644 --- a/apps/server/src/api-data/automation/automation.validation.ts +++ b/apps/server/src/api-data/automation/automation.validation.ts @@ -10,22 +10,12 @@ import { } from 'ontime-types'; import { parseUserTime } from 'ontime-utils'; -import type { Request, Response, NextFunction } from 'express'; -import { body, oneOf, param, validationResult } from 'express-validator'; +import { body, oneOf, param } from 'express-validator'; import * as assert from '../../utils/assert.js'; import { isFilterOperator, isFilterRule, isOntimeActionAction } from './automation.utils.js'; - -export const paramContainsId = [ - param('id').isString().notEmpty(), - - (req: Request, res: Response, next: NextFunction) => { - const errors = validationResult(req); - if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); - next(); - }, -]; +import { requestValidationFunction } from '../validation-utils/validationFunction.js'; export const validateAutomationSettings = [ body('enabledAutomations').isBoolean(), @@ -37,57 +27,33 @@ export const validateAutomationSettings = [ body('triggers.*.automationId').optional().isString().trim(), body('automations').optional().custom(parseAutomation), - (req: Request, res: Response, next: NextFunction) => { - const errors = validationResult(req); - if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); - next(); - }, + requestValidationFunction, ]; export const validateTrigger = [ - body('title').isString().trim(), + body('title').isString().trim().notEmpty(), body('trigger').isIn(timerLifecycleValues), - body('automationId').isString().trim(), + body('automationId').isString().trim().notEmpty(), - (req: Request, res: Response, next: NextFunction) => { - const errors = validationResult(req); - if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); - next(); - }, + requestValidationFunction, ]; export const validateTriggerPatch = [ param('id').isString().notEmpty(), - body('title').optional().isString().trim(), + body('title').optional().isString().trim().notEmpty(), body('trigger').optional().isIn(timerLifecycleValues), - body('automationId').optional().isString().trim(), + body('automationId').optional().isString().trim().notEmpty(), - (req: Request, res: Response, next: NextFunction) => { - const errors = validationResult(req); - if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); - next(); - }, + requestValidationFunction, ]; -export const validateAutomation = [ - body().custom(parseAutomation), - - (req: Request, res: Response, next: NextFunction) => { - const errors = validationResult(req); - if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); - next(); - }, -]; +export const validateAutomation = [body().custom(parseAutomation), requestValidationFunction]; export const validateAutomationPatch = [ param('id').isString().notEmpty(), body().custom(parseAutomation), - (req: Request, res: Response, next: NextFunction) => { - const errors = validationResult(req); - if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); - next(); - }, + requestValidationFunction, ]; /** @@ -162,11 +128,7 @@ export const validateTestPayload = [ body('visible').if(body('type').equals('ontime')).optional().isString().trim(), body('secondarySource').if(body('type').equals('ontime')).optional().isString().trim(), - (req: Request, res: Response, next: NextFunction) => { - const errors = validationResult(req); - if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); - next(); - }, + requestValidationFunction, ]; /** diff --git a/apps/server/src/api-data/custom-fields/customFields.router.ts b/apps/server/src/api-data/custom-fields/customFields.router.ts index 452c0c930..7de1de47e 100644 --- a/apps/server/src/api-data/custom-fields/customFields.router.ts +++ b/apps/server/src/api-data/custom-fields/customFields.router.ts @@ -13,7 +13,7 @@ export const router = express.Router(); router.get('/', async (_req: Request, res: Response) => { const customFields = getProjectCustomFields(); - res.json(customFields); + res.status(200).json(customFields); }); router.post('/', validateCustomField, async (req: Request, res: Response) => { diff --git a/apps/server/src/api-data/custom-fields/customFields.validation.ts b/apps/server/src/api-data/custom-fields/customFields.validation.ts index 7b2b0734a..f63250919 100644 --- a/apps/server/src/api-data/custom-fields/customFields.validation.ts +++ b/apps/server/src/api-data/custom-fields/customFields.validation.ts @@ -1,7 +1,7 @@ import { isAlphanumericWithSpace } from 'ontime-utils'; -import type { Request, Response, NextFunction } from 'express'; -import { body, param, validationResult } from 'express-validator'; +import { body, param } from 'express-validator'; +import { requestValidationFunction } from '../validation-utils/validationFunction.js'; export const validateCustomField = [ body('label') @@ -14,11 +14,7 @@ export const validateCustomField = [ body('type').isIn(['string', 'image']), body('colour').isString().trim(), - (req: Request, res: Response, next: NextFunction) => { - const errors = validationResult(req); - if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); - next(); - }, + requestValidationFunction, ]; export const validateEditCustomField = [ @@ -33,19 +29,7 @@ export const validateEditCustomField = [ body('type').isIn(['string', 'image']), body('colour').isString().trim(), - (req: Request, res: Response, next: NextFunction) => { - const errors = validationResult(req); - if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); - next(); - }, + requestValidationFunction, ]; -export const validateDeleteCustomField = [ - param('key').isString().notEmpty(), - - (req: Request, res: Response, next: NextFunction) => { - const errors = validationResult(req); - if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); - next(); - }, -]; +export const validateDeleteCustomField = [param('key').isString().notEmpty(), requestValidationFunction]; diff --git a/apps/server/src/api-data/db/db.controller.ts b/apps/server/src/api-data/db/db.controller.ts index a56370628..1e27c2deb 100644 --- a/apps/server/src/api-data/db/db.controller.ts +++ b/apps/server/src/api-data/db/db.controller.ts @@ -106,7 +106,8 @@ export async function projectDownload(req: Request, res: Response) { const { filename } = req.body; const pathToFile = doesProjectExist(filename); if (!pathToFile) { - return res.status(404).send({ message: `Project ${filename} not found.` }); + res.status(404).send({ message: `Project ${filename} not found.` }); + return; } res.download(pathToFile, filename, (error) => { @@ -138,7 +139,8 @@ export async function postProjectFile(req: Request, res: Response { - const errors = validationResult(req); - if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); - next(); - }, + requestValidationFunction, ]; /** @@ -40,25 +37,14 @@ export const validateQuickProject = [ body('viewSettings.freezeEnd').optional().isBoolean(), body('viewSettings.endMessage').optional().isString().trim(), - (req: Request, res: Response, next: NextFunction) => { - const errors = validationResult(req); - if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); - next(); - }, + requestValidationFunction, ]; /** * @description Validates request for pathing data in the project. */ export const validatePatchProject = [ - // Custom validator to ensure the body is not empty - (req: Request, res: Response, next: NextFunction) => { - if (Object.keys(req.body).length === 0) { - return res.status(422).json({ errors: [{ msg: 'Request body cannot be empty' }] }); - } - next(); - }, - + body().notEmpty().withMessage('No object found in request'), body('rundowns').isObject().optional({ nullable: false }), body('project').isObject().optional({ nullable: false }), body('settings').isObject().optional({ nullable: false }), @@ -68,11 +54,7 @@ export const validatePatchProject = [ body('osc').isObject().optional({ nullable: false }), body('http').isObject().optional({ nullable: false }), - (req: Request, res: Response, next: NextFunction) => { - const errors = validationResult(req); - if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); - next(); - }, + requestValidationFunction, ]; /** @@ -88,14 +70,7 @@ export const validateNewFilenameBody = [ .withMessage('Filename was empty or contained only invalid characters') .customSanitizer((input: string) => ensureJsonExtension(input)), - (req: Request, res: Response, next: NextFunction) => { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(422).json({ errors: errors.array() }); - } - - next(); - }, + requestValidationFunction, ]; /** @@ -111,14 +86,7 @@ export const validateFilenameBody = [ .withMessage('Filename was empty or contained only invalid characters') .customSanitizer((input: string) => ensureJsonExtension(input)), - (req: Request, res: Response, next: NextFunction) => { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(422).json({ errors: errors.array() }); - } - - next(); - }, + requestValidationFunction, ]; /** @@ -134,12 +102,5 @@ export const validateFilenameParam = [ .withMessage('Filename was empty or contained only invalid characters') .customSanitizer((input: string) => ensureJsonExtension(input)), - (req: Request, res: Response, next: NextFunction) => { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(422).json({ errors: errors.array() }); - } - - next(); - }, + requestValidationFunction, ]; diff --git a/apps/server/src/api-data/excel/excel.controller.ts b/apps/server/src/api-data/excel/excel.controller.ts deleted file mode 100644 index 16d2e5072..000000000 --- a/apps/server/src/api-data/excel/excel.controller.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * This module encapsulates logic related to - * Google Sheets - */ - -import type { Request, Response } from 'express'; -import { generateRundownPreview, listWorksheets, saveExcelFile } from './excel.service.js'; -import { CustomFields, Rundown } from 'ontime-types'; - -export async function postExcel(req: Request, res: Response) { - try { - // file has been validated by middleware - const filePath = (req.file as Express.Multer.File).path; - await saveExcelFile(filePath); - res.status(200).send(); - } catch (error) { - res.status(500).send({ message: String(error) }); - } -} - -export async function getWorksheets(req: Request, res: Response) { - try { - const names = listWorksheets(); - res.status(200).send(names); - } catch (error) { - res.status(500).send({ message: String(error) }); - } -} - -/** - * parses an Excel spreadsheet - * @returns parsed result - */ -export async function previewExcel( - req: Request, - res: Response<{ rundown: Rundown; customFields: CustomFields } | { message: string }>, -) { - try { - const { options } = req.body; - const data = generateRundownPreview(options); - res.status(200).send(data); - } catch (error) { - res.status(500).send({ message: String(error) }); - } -} diff --git a/apps/server/src/api-data/excel/excel.router.ts b/apps/server/src/api-data/excel/excel.router.ts index 5e8541af6..cce0a46e4 100644 --- a/apps/server/src/api-data/excel/excel.router.ts +++ b/apps/server/src/api-data/excel/excel.router.ts @@ -1,14 +1,42 @@ -/** - * This is a feature specific router for integration with Excel - */ - import express from 'express'; +import type { Request, Response } from 'express'; import { uploadExcel } from './excel.middleware.js'; -import { getWorksheets, postExcel, previewExcel } from './excel.controller.js'; import { validateFileExists, validateImportMapOptions } from './excel.validation.js'; +import { CustomFields, ErrorResponse, Rundown } from 'ontime-types'; +import { generateRundownPreview, listWorksheets, saveExcelFile } from './excel.service.js'; export const router = express.Router(); -router.post('/upload', uploadExcel, validateFileExists, postExcel); -router.get('/worksheets', getWorksheets); -router.post('/preview', validateImportMapOptions, previewExcel); +router.post('/upload', uploadExcel, validateFileExists, async (req: Request, res: Response) => { + try { + // file has been validated by middleware + const filePath = (req.file as Express.Multer.File).path; + await saveExcelFile(filePath); + res.status(201).send(); + } catch (error) { + res.status(500).send({ message: String(error) }); + } +}); + +router.get('/worksheets', (_req: Request, res: Response) => { + try { + const names = listWorksheets(); + res.status(200).send(names); + } catch (error) { + res.status(500).send({ message: String(error) }); + } +}); + +router.post( + '/preview', + validateImportMapOptions, + (req: Request, res: Response<{ rundown: Rundown; customFields: CustomFields } | ErrorResponse>) => { + try { + const { options } = req.body; + const data = generateRundownPreview(options); + res.status(200).send(data); + } catch (error) { + res.status(500).send({ message: String(error) }); + } + }, +); diff --git a/apps/server/src/api-data/excel/excel.validation.ts b/apps/server/src/api-data/excel/excel.validation.ts index c294b5d39..48924a45b 100644 --- a/apps/server/src/api-data/excel/excel.validation.ts +++ b/apps/server/src/api-data/excel/excel.validation.ts @@ -1,16 +1,12 @@ import { isImportMap } from 'ontime-utils'; -import { body, validationResult } from 'express-validator'; -import type { NextFunction, Request, Response } from 'express'; +import { body } from 'express-validator'; +import { + requestValidationFunction, + requestValidationFunctionWithFile, +} from '../validation-utils/validationFunction.js'; -export const validateFileExists = [ - (req: Request, res: Response, next: NextFunction) => { - if (!req.file) { - return res.status(422).json({ errors: 'File not found' }); - } - next(); - }, -]; +export const validateFileExists = [requestValidationFunctionWithFile]; export const validateImportMapOptions = [ body('options') @@ -19,9 +15,5 @@ export const validateImportMapOptions = [ return isImportMap(content); }), - (req: Request, res: Response, next: NextFunction) => { - const errors = validationResult(req); - if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); - next(); - }, + requestValidationFunction, ]; diff --git a/apps/server/src/api-data/index.ts b/apps/server/src/api-data/index.ts index ac62b6ca1..cfc61e135 100644 --- a/apps/server/src/api-data/index.ts +++ b/apps/server/src/api-data/index.ts @@ -31,6 +31,6 @@ appRouter.use('/report', reportRouter); appRouter.use('/assets', assetsRouter); //we don't want to redirect to react index when using api routes -appRouter.all('/*', (_req, res) => { - res.status(404).send(); +appRouter.all('/*splat', (_req, res) => { + res.status(404).send('data path not found'); }); diff --git a/apps/server/src/api-data/project-data/projectData.controller.ts b/apps/server/src/api-data/project-data/projectData.controller.ts deleted file mode 100644 index c66bc6cd9..000000000 --- a/apps/server/src/api-data/project-data/projectData.controller.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { ErrorResponse, ProjectData } from 'ontime-types'; -import { getErrorMessage } from 'ontime-utils'; - -import type { Request, Response } from 'express'; - -import { removeUndefined } from '../../utils/parserUtils.js'; -import { failEmptyObjects } from '../../utils/routerUtils.js'; -import { editCurrentProjectData } from '../../services/project-service/ProjectService.js'; -import * as projectDao from './projectData.dao.js'; - -export function getProjectData(_req: Request, res: Response) { - res.json(projectDao.getProjectData()); -} - -export async function postProjectData(req: Request, res: Response) { - if (failEmptyObjects(req.body, res)) { - return; - } - - try { - const newData: Partial = removeUndefined({ - title: req.body?.title, - description: req.body?.description, - publicUrl: req.body?.publicUrl, - publicInfo: req.body?.publicInfo, - backstageUrl: req.body?.backstageUrl, - backstageInfo: req.body?.backstageInfo, - endMessage: req.body?.endMessage, - projectLogo: req.body?.projectLogo, - custom: req.body?.custom, - }); - - const updatedData = await editCurrentProjectData(newData); - - res.status(200).send(updatedData); - } catch (error) { - const message = getErrorMessage(error); - res.status(400).send({ message }); - } -} diff --git a/apps/server/src/api-data/project-data/projectData.router.ts b/apps/server/src/api-data/project-data/projectData.router.ts index 513b79a94..3b55e6c09 100644 --- a/apps/server/src/api-data/project-data/projectData.router.ts +++ b/apps/server/src/api-data/project-data/projectData.router.ts @@ -1,12 +1,42 @@ import express from 'express'; +import type { Request, Response } from 'express'; +import type { ErrorResponse, ProjectData } from 'ontime-types'; +import { getErrorMessage } from 'ontime-utils'; -import { getProjectData, postProjectData } from './projectData.controller.js'; import { projectSanitiser } from './projectData.validation.js'; import { uploadImageFile } from '../db/db.middleware.js'; import { postProjectLogo } from '../db/db.controller.js'; +import * as projectDao from './projectData.dao.js'; +import { removeUndefined } from '../../utils/parserUtils.js'; +import { editCurrentProjectData } from '../../services/project-service/ProjectService.js'; export const router = express.Router(); -router.get('/', getProjectData); -router.post('/', projectSanitiser, postProjectData); +router.get('/', (_req: Request, res: Response) => { + res.status(200).json(projectDao.getProjectData()); +}); + +router.post('/', projectSanitiser, async (req: Request, res: Response) => { + try { + const newData: Partial = removeUndefined({ + title: req.body?.title, + description: req.body?.description, + publicUrl: req.body?.publicUrl, + publicInfo: req.body?.publicInfo, + backstageUrl: req.body?.backstageUrl, + backstageInfo: req.body?.backstageInfo, + endMessage: req.body?.endMessage, + projectLogo: req.body?.projectLogo, + custom: req.body?.custom, + }); + + const updatedData = await editCurrentProjectData(newData); + + res.status(200).send(updatedData); + } catch (error) { + const message = getErrorMessage(error); + res.status(400).send({ message }); + } +}); + router.post('/upload', uploadImageFile, postProjectLogo); diff --git a/apps/server/src/api-data/project-data/projectData.validation.ts b/apps/server/src/api-data/project-data/projectData.validation.ts index 4014d2d31..8704c24ce 100644 --- a/apps/server/src/api-data/project-data/projectData.validation.ts +++ b/apps/server/src/api-data/project-data/projectData.validation.ts @@ -1,7 +1,8 @@ -import { Request, Response, NextFunction } from 'express'; -import { body, validationResult } from 'express-validator'; +import { body } from 'express-validator'; +import { requestValidationFunction } from '../validation-utils/validationFunction.js'; export const projectSanitiser = [ + body().notEmpty().withMessage('No object found in request'), body('title').optional().isString().trim(), body('description').optional().isString().trim(), body('publicUrl').optional().isString().trim(), @@ -9,14 +10,10 @@ export const projectSanitiser = [ body('backstageUrl').optional().isString().trim(), body('backstageInfo').optional().isString().trim(), body('endMessage').optional().isString().trim(), - body('projectLogo').optional({ nullable: true }).isString().trim(), + body('projectLogo').optional({ nullable: true }).isString().trim().isBase64(), body('custom').optional().isArray(), body('custom.*.title').optional().isString().trim().notEmpty(), body('custom.*.value').optional().isString().trim().notEmpty(), - (req: Request, res: Response, next: NextFunction) => { - const errors = validationResult(req); - if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); - next(); - }, + requestValidationFunction, ]; diff --git a/apps/server/src/api-data/report/report.controller.ts b/apps/server/src/api-data/report/report.controller.ts deleted file mode 100644 index a81a2f5b2..000000000 --- a/apps/server/src/api-data/report/report.controller.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { Request, Response } from 'express'; -import type { OntimeReport } from 'ontime-types'; -import * as report from './report.service.js'; - -export function getAll(_req: Request, res: Response) { - res.json(report.generate()); -} - -export function deleteAll(_req: Request, res: Response) { - report.clear(); - res.status(200).send(); -} - -export function deleteWithId(req: Request, res: Response) { - const { eventId } = req.params; - report.clear(eventId); - res.status(200).send(); -} diff --git a/apps/server/src/api-data/report/report.router.ts b/apps/server/src/api-data/report/report.router.ts index 7c8445473..2d6ad3736 100644 --- a/apps/server/src/api-data/report/report.router.ts +++ b/apps/server/src/api-data/report/report.router.ts @@ -1,10 +1,21 @@ import express from 'express'; -import { getAll, deleteWithId, deleteAll } from './report.controller.js'; -import { paramsMustHaveEntryId } from '../rundown/rundown.validation.js'; +import type { Request, Response } from 'express'; +import { paramsWithId } from '../validation-utils/validationFunction.js'; +import * as report from './report.service.js'; export const router = express.Router(); -router.get('/', getAll); +router.get('/', (_req: Request, res: Response) => { + res.status(200).json(report.generate()); +}); -router.delete('/all', deleteAll); -router.delete('/:eventId', paramsMustHaveEntryId, deleteWithId); +router.delete('/all', (_req: Request, res: Response) => { + report.clear(); + res.status(204).send(); +}); + +router.delete('/:id', paramsWithId, (req: Request, res: Response) => { + const { id } = req.params; + report.clear(id); + res.status(204).send(); +}); diff --git a/apps/server/src/api-data/rundown/rundown.router.ts b/apps/server/src/api-data/rundown/rundown.router.ts index dcda09708..a75a55463 100644 --- a/apps/server/src/api-data/rundown/rundown.router.ts +++ b/apps/server/src/api-data/rundown/rundown.router.ts @@ -19,7 +19,6 @@ import { ungroupEntries, } from './rundown.service.js'; import { - paramsMustHaveEntryId, rundownArrayOfIds, rundownBatchPutValidator, rundownPostValidator, @@ -27,6 +26,7 @@ import { rundownReorderValidator, rundownSwapValidator, } from './rundown.validation.js'; +import { paramsWithId } from '../validation-utils/validationFunction.js'; export const router = express.Router(); @@ -100,11 +100,11 @@ router.patch('/swap', rundownSwapValidator, async (req: Request, res: Response) => { try { - const newRundown = await applyDelay(req.params.entryId); + const newRundown = await applyDelay(req.params.id); res.status(200).send(newRundown); } catch (error) { const message = getErrorMessage(error); @@ -113,9 +113,9 @@ router.patch( }, ); -router.post('/clone/:entryId', paramsMustHaveEntryId, async (req: Request, res: Response) => { +router.post('/clone/:id', paramsWithId, async (req: Request, res: Response) => { try { - const newRundown = await cloneEntry(req.params.entryId); + const newRundown = await cloneEntry(req.params.id); res.status(200).send(newRundown); } catch (error) { const message = getErrorMessage(error); @@ -134,11 +134,11 @@ router.post('/group', rundownArrayOfIds, async (req: Request, res: Response) => { try { - const newRundown = await ungroupEntries(req.params.entryId); + const newRundown = await ungroupEntries(req.params.id); res.status(200).send(newRundown); } catch (error) { const message = getErrorMessage(error); diff --git a/apps/server/src/api-data/rundown/rundown.validation.ts b/apps/server/src/api-data/rundown/rundown.validation.ts index 933ba2adc..f42efbb77 100644 --- a/apps/server/src/api-data/rundown/rundown.validation.ts +++ b/apps/server/src/api-data/rundown/rundown.validation.ts @@ -1,38 +1,22 @@ -import { body, param, validationResult } from 'express-validator'; -import type { Request, Response, NextFunction } from 'express'; +import { body, param } from 'express-validator'; +import { requestValidationFunction } from '../validation-utils/validationFunction.js'; export const rundownPostValidator = [ body('type').isString().isIn(['event', 'delay', 'block']), body('after').optional().isString(), body('before').optional().isString(), - (req: Request, res: Response, next: NextFunction) => { - const errors = validationResult(req); - if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); - next(); - }, + requestValidationFunction, ]; -export const rundownPutValidator = [ - body('id').isString().notEmpty(), - - (req: Request, res: Response, next: NextFunction) => { - const errors = validationResult(req); - if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); - next(); - }, -]; +export const rundownPutValidator = [body('id').isString().notEmpty(), requestValidationFunction]; export const rundownBatchPutValidator = [ body('data').isObject(), body('ids').isArray().notEmpty(), body('ids.*').isString(), - (req: Request, res: Response, next: NextFunction) => { - const errors = validationResult(req); - if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); - next(); - }, + requestValidationFunction, ]; export const rundownReorderValidator = [ @@ -40,41 +24,21 @@ export const rundownReorderValidator = [ body('destinationId').isString().notEmpty(), body('order').isIn(['before', 'after', 'insert']), - (req: Request, res: Response, next: NextFunction) => { - const errors = validationResult(req); - if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); - next(); - }, + requestValidationFunction, ]; export const rundownSwapValidator = [ body('from').isString().notEmpty(), body('to').isString().notEmpty(), - (req: Request, res: Response, next: NextFunction) => { - const errors = validationResult(req); - if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); - next(); - }, + requestValidationFunction, ]; -export const paramsMustHaveEntryId = [ - param('entryId').isString().notEmpty(), - - (req: Request, res: Response, next: NextFunction) => { - const errors = validationResult(req); - if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); - next(); - }, -]; +export const paramsMustHaveEntryId = [param('entryId').isString().notEmpty(), requestValidationFunction]; export const rundownArrayOfIds = [ body('ids').isArray().notEmpty(), body('ids.*').isString(), - (req: Request, res: Response, next: NextFunction) => { - const errors = validationResult(req); - if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); - next(); - }, + requestValidationFunction, ]; diff --git a/apps/server/src/api-data/session/session.controller.ts b/apps/server/src/api-data/session/session.controller.ts deleted file mode 100644 index 8d2ba9957..000000000 --- a/apps/server/src/api-data/session/session.controller.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { getErrorMessage } from 'ontime-utils'; -import { ErrorResponse, GetInfo, GetUrl, SessionStats } from 'ontime-types'; - -import type { Request, Response } from 'express'; - -import * as sessionService from './session.service.js'; - -export async function getSessionStats(_req: Request, res: Response) { - try { - const stats = await sessionService.getSessionStats(); - res.status(200).send(stats); - } catch (error) { - const message = getErrorMessage(error); - res.status(500).send({ message }); - } -} - -export async function getInfo(_req: Request, res: Response) { - try { - const info = await sessionService.getInfo(); - res.status(200).send(info); - } catch (error) { - const message = getErrorMessage(error); - res.status(500).send({ message }); - } -} - -export async function generateUrl(req: Request, res: Response) { - try { - const url = sessionService.generateAuthenticatedUrl( - req.body.baseUrl, - req.body.path, - req.body.lock, - req.body.authenticate, - ); - res.status(200).send({ url: url.toString() }); - } catch (error) { - const message = getErrorMessage(error); - res.status(500).send({ message }); - } -} diff --git a/apps/server/src/api-data/session/session.router.ts b/apps/server/src/api-data/session/session.router.ts index 3f37c3db6..61de685ee 100644 --- a/apps/server/src/api-data/session/session.router.ts +++ b/apps/server/src/api-data/session/session.router.ts @@ -1,10 +1,43 @@ import express from 'express'; - -import { getInfo, getSessionStats, generateUrl } from './session.controller.js'; +import type { Request, Response } from 'express'; +import { getErrorMessage } from 'ontime-utils'; +import type { ErrorResponse, GetInfo, GetUrl, SessionStats } from 'ontime-types'; import { validateGenerateUrl } from './session.validation.js'; +import * as sessionService from './session.service.js'; export const router = express.Router(); -router.get('/', getSessionStats); -router.get('/info', getInfo); -router.post('/url', validateGenerateUrl, generateUrl); +router.get('/', async (_req: Request, res: Response) => { + try { + const stats = await sessionService.getSessionStats(); + res.status(200).send(stats); + } catch (error) { + const message = getErrorMessage(error); + res.status(500).send({ message }); + } +}); + +router.get('/info', async (_req: Request, res: Response) => { + try { + const info = await sessionService.getInfo(); + res.status(200).send(info); + } catch (error) { + const message = getErrorMessage(error); + res.status(500).send({ message }); + } +}); + +router.post('/url', validateGenerateUrl, (req: Request, res: Response) => { + try { + const url = sessionService.generateAuthenticatedUrl( + req.body.baseUrl, + req.body.path, + req.body.lock, + req.body.authenticate, + ); + res.status(200).send({ url: url.toString() }); + } catch (error) { + const message = getErrorMessage(error); + res.status(500).send({ message }); + } +}); diff --git a/apps/server/src/api-data/session/session.validation.ts b/apps/server/src/api-data/session/session.validation.ts index a575a544f..83db92689 100644 --- a/apps/server/src/api-data/session/session.validation.ts +++ b/apps/server/src/api-data/session/session.validation.ts @@ -1,5 +1,6 @@ -import type { Request, Response, NextFunction } from 'express'; -import { body, validationResult } from 'express-validator'; + +import { body } from 'express-validator'; +import { requestValidationFunction } from '../validation-utils/validationFunction.js'; export const validateGenerateUrl = [ body('baseUrl').isString().trim().notEmpty(), @@ -7,9 +8,5 @@ export const validateGenerateUrl = [ body('lock').isBoolean(), body('authenticate').isBoolean(), - (req: Request, res: Response, next: NextFunction) => { - const errors = validationResult(req); - if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); - next(); - }, + requestValidationFunction, ]; diff --git a/apps/server/src/api-data/settings/settings.controller.ts b/apps/server/src/api-data/settings/settings.controller.ts index 84b541e1d..384aaa00a 100644 --- a/apps/server/src/api-data/settings/settings.controller.ts +++ b/apps/server/src/api-data/settings/settings.controller.ts @@ -4,7 +4,6 @@ import { getErrorMessage, obfuscate } from 'ontime-utils'; import type { Request, Response } from 'express'; import { isDocker } from '../../setup/environment.js'; -import { failEmptyObjects } from '../../utils/routerUtils.js'; import { getDataProvider } from '../../classes/data-provider/DataProvider.js'; import * as appState from '../../services/app-state-service/AppStateService.js'; @@ -25,9 +24,6 @@ export async function getSettings(_req: Request, res: Response) { } export async function postSettings(req: Request, res: Response) { - if (failEmptyObjects(req.body, res)) { - return; - } try { const settings = getDataProvider().getSettings(); const editorKey = extractPin(req.body?.editorKey, settings.editorKey); @@ -35,13 +31,15 @@ export async function postSettings(req: Request, res: Response { - const errors = validationResult(req); - if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); - next(); - }, -]; +export const validateWelcomeDialog = [body('show').isBoolean(), requestValidationFunction]; /** * @description Validates object for POST /ontime/settings */ export const validateSettings = [ + body().notEmpty().withMessage('No object found in request'), body('editorKey').isString().isLength({ min: 0, max: 4 }).optional({ nullable: true }), body('operatorKey').isString().isLength({ min: 0, max: 4 }).optional({ nullable: true }), body('timeFormat').isString().isIn(['12', '24']), body('language').isString(), body('serverPort').isPort().optional(), - (req: Request, res: Response, next: NextFunction) => { - const errors = validationResult(req); - if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); - next(); - }, + requestValidationFunction, ]; diff --git a/apps/server/src/api-data/sheets/sheets.validation.ts b/apps/server/src/api-data/sheets/sheets.validation.ts index d0767a377..bf687c8fb 100644 --- a/apps/server/src/api-data/sheets/sheets.validation.ts +++ b/apps/server/src/api-data/sheets/sheets.validation.ts @@ -2,6 +2,7 @@ import { isImportMap } from 'ontime-utils'; import { body, param, validationResult } from 'express-validator'; import { NextFunction, Request, Response } from 'express'; +import { requestValidationFunction } from '../validation-utils/validationFunction.js'; export const validateRequestConnection = [ param('sheetId') @@ -14,27 +15,23 @@ export const validateRequestConnection = [ (req: Request, res: Response, next: NextFunction) => { const errors = validationResult(req); - if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); + if (!errors.isEmpty()) { + res.status(422).json({ errors: errors.array() }); + return; + } // check that the file exists if (!req.file) { - return res.status(422).json({ errors: 'File not found' }); + res.status(422).json({ errors: 'File not found' }); + return; } next(); }, ]; -export const validateSheetId = [ - param('sheetId').isString(), - - (req: Request, res: Response, next: NextFunction) => { - const errors = validationResult(req); - if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); - next(); - }, -]; +export const validateSheetId = [param('sheetId').isString().trim().notEmpty(), requestValidationFunction]; export const validateSheetOptions = [ - param('sheetId').isString(), + param('sheetId').isString().trim().notEmpty(), body('options') .isObject() .custom((content) => { @@ -42,9 +39,5 @@ export const validateSheetOptions = [ return isValid; }), - (req: Request, res: Response, next: NextFunction) => { - const errors = validationResult(req); - if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); - next(); - }, + requestValidationFunction, ]; diff --git a/apps/server/src/api-data/url-presets/urlPresets.controller.ts b/apps/server/src/api-data/url-presets/urlPresets.controller.ts deleted file mode 100644 index fb763c29a..000000000 --- a/apps/server/src/api-data/url-presets/urlPresets.controller.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { ErrorResponse, URLPreset } from 'ontime-types'; -import { getErrorMessage } from 'ontime-utils'; - -import type { Request, Response } from 'express'; - -import { failIsNotArray } from '../../utils/routerUtils.js'; -import { getDataProvider } from '../../classes/data-provider/DataProvider.js'; - -export async function getUrlPresets(_req: Request, res: Response) { - const presets = getDataProvider().getUrlPresets(); - res.status(200).send(presets as URLPreset[]); -} - -export async function postUrlPresets(req: Request, res: Response) { - if (failIsNotArray(req.body, res)) { - return; - } - try { - const newPresets: URLPreset[] = req.body.map((preset: URLPreset) => ({ - enabled: preset.enabled, - alias: preset.alias, - pathAndParams: preset.pathAndParams, - })); - await getDataProvider().setUrlPresets(newPresets); - res.status(200).send(newPresets); - } catch (error) { - const message = getErrorMessage(error); - res.status(400).send({ message }); - } -} diff --git a/apps/server/src/api-data/url-presets/urlPresets.router.ts b/apps/server/src/api-data/url-presets/urlPresets.router.ts index 65582dd1b..765f2aedd 100644 --- a/apps/server/src/api-data/url-presets/urlPresets.router.ts +++ b/apps/server/src/api-data/url-presets/urlPresets.router.ts @@ -1,8 +1,28 @@ import express from 'express'; -import { getUrlPresets, postUrlPresets } from './urlPresets.controller.js'; +import type { Request, Response } from 'express'; +import type { ErrorResponse, URLPreset } from 'ontime-types'; +import { getErrorMessage } from 'ontime-utils'; import { validateUrlPresets } from './urlPresets.validation.js'; +import { getDataProvider } from '../../classes/data-provider/DataProvider.js'; export const router = express.Router(); -router.get('/', getUrlPresets); -router.post('/', validateUrlPresets, postUrlPresets); +router.get('/', (_req: Request, res: Response) => { + const presets = getDataProvider().getUrlPresets(); + res.status(200).send(presets as URLPreset[]); +}); + +router.post('/', validateUrlPresets, async (req: Request, res: Response) => { + try { + const newPresets: URLPreset[] = req.body.map((preset: URLPreset) => ({ + enabled: preset.enabled, + alias: preset.alias, + pathAndParams: preset.pathAndParams, + })); + await getDataProvider().setUrlPresets(newPresets); + res.status(200).send(newPresets); + } catch (error) { + const message = getErrorMessage(error); + res.status(400).send({ message }); + } +}); diff --git a/apps/server/src/api-data/url-presets/urlPresets.validation.ts b/apps/server/src/api-data/url-presets/urlPresets.validation.ts index f267ad959..fdf8a8ad2 100644 --- a/apps/server/src/api-data/url-presets/urlPresets.validation.ts +++ b/apps/server/src/api-data/url-presets/urlPresets.validation.ts @@ -1,18 +1,14 @@ -import { body, validationResult } from 'express-validator'; -import { Request, Response, NextFunction } from 'express'; +import { body } from 'express-validator'; +import { requestValidationFunction } from '../validation-utils/validationFunction.js'; /** * validate array of URL preset objects */ export const validateUrlPresets = [ - body().isArray(), + body().isArray().withMessage('No array found in request'), body('*.enabled').isBoolean(), - body('*.alias').isString().trim(), - body('*.pathAndParams').isString().trim(), + body('*.alias').isString().trim().notEmpty(), + body('*.pathAndParams').isString().trim().notEmpty(), - (req: Request, res: Response, next: NextFunction) => { - const errors = validationResult(req); - if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); - next(); - }, + requestValidationFunction, ]; diff --git a/apps/server/src/api-data/validation-utils/validationFunction.ts b/apps/server/src/api-data/validation-utils/validationFunction.ts new file mode 100644 index 000000000..bc8f8903a --- /dev/null +++ b/apps/server/src/api-data/validation-utils/validationFunction.ts @@ -0,0 +1,34 @@ +import type { Request, Response, NextFunction } from 'express'; +import { param, validationResult } from 'express-validator'; + +/** + * Runs validation and any error are sent with status 422 + */ +export function requestValidationFunction(req: Request, res: Response, next: NextFunction) { + const errors = validationResult(req); + if (!errors.isEmpty()) { + res.status(422).json({ errors: errors.array() }); + return; + } + next(); +} + +/** + * Runs validation and any error are sent with status 422 + * Also checks for the presses of a `file` in the body + */ +export function requestValidationFunctionWithFile(req: Request, res: Response, next: NextFunction) { + const errors = validationResult(req); + if (!errors.isEmpty()) { + res.status(422).json({ errors: errors.array() }); + return; + } + // check that the file exists + if (!req.file) { + res.status(422).json({ errors: 'File not found' }); + return; + } + next(); +} + +export const paramsWithId = [param('id').isString().trim().notEmpty(), requestValidationFunction]; diff --git a/apps/server/src/api-data/view-settings/viewSettings.validation.ts b/apps/server/src/api-data/view-settings/viewSettings.validation.ts index abe8a56b6..3c42b20d3 100644 --- a/apps/server/src/api-data/view-settings/viewSettings.validation.ts +++ b/apps/server/src/api-data/view-settings/viewSettings.validation.ts @@ -1,20 +1,16 @@ -import { check, validationResult } from 'express-validator'; -import { Request, Response, NextFunction } from 'express'; +import { body } from 'express-validator'; +import { requestValidationFunction } from '../validation-utils/validationFunction.js'; /** * @description Validates object for POST /ontime/views */ export const validateViewSettings = [ - check('dangerColor').isString().trim().withMessage('dangerColor value must be string'), - check('endMessage').isString().trim().withMessage('endMessage value must be string'), - check('freezeEnd').isBoolean().withMessage('freezeEnd value must be boolean'), - check('normalColor').isString().trim().withMessage('normalColor value must be string'), - check('overrideStyles').isBoolean().withMessage('overrideStyles value must be boolean'), - check('warningColor').isString().trim().withMessage('warningColor value must be string'), + body('dangerColor').isString().trim().withMessage('dangerColor value must be string'), + body('endMessage').isString().trim().withMessage('endMessage value must be string'), + body('freezeEnd').isBoolean().withMessage('freezeEnd value must be boolean'), + body('normalColor').isString().trim().withMessage('normalColor value must be string'), + body('overrideStyles').isBoolean().withMessage('overrideStyles value must be boolean'), + body('warningColor').isString().trim().withMessage('warningColor value must be string'), - (req: Request, res: Response, next: NextFunction) => { - const errors = validationResult(req); - if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() }); - next(); - }, + requestValidationFunction, ]; diff --git a/apps/server/src/api-integration/integration.router.ts b/apps/server/src/api-integration/integration.router.ts index a403794bb..db491247a 100644 --- a/apps/server/src/api-integration/integration.router.ts +++ b/apps/server/src/api-integration/integration.router.ts @@ -5,7 +5,7 @@ * */ -import { LogOrigin } from 'ontime-types'; +import { ErrorResponse, LogOrigin } from 'ontime-types'; import express, { type Request, type Response } from 'express'; @@ -27,10 +27,11 @@ integrationRouter.get('/', (_req: Request, res: Response<{ message: string }>) = /** * All calls are sent to the dispatcher */ -integrationRouter.get('/*', (req: Request, res: Response) => { +integrationRouter.get('/*splat', (req: Request, res: Response) => { let action = req.path.substring(1); if (!action) { - return res.status(400).json({ error: 'No action found' }); + res.status(400).json({ message: 'No action found' }); + return; } try { diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index 7938f975a..96d843e00 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -76,7 +76,7 @@ app.disable('x-powered-by'); // Implement middleware app.use(cors()); // setup cors for all routes -app.options('*', cors()); // enable pre-flight cors +app.options('*splat', cors()); // enable pre-flight cors app.use(bodyParser); app.use(cookieParser()); @@ -97,7 +97,7 @@ app.use(`${prefix}/user`, express.static(publicDir.userDir)); // Base route for static files app.use(`${prefix}`, authenticateAndRedirect, compressedStatic); -app.use(`${prefix}/*`, authenticateAndRedirect, compressedStatic); +app.use(`${prefix}/*splat`, authenticateAndRedirect, compressedStatic); // Implement catch all app.use((_error, response) => { diff --git a/apps/server/src/utils/routerUtils.ts b/apps/server/src/utils/routerUtils.ts deleted file mode 100644 index 77b987169..000000000 --- a/apps/server/src/utils/routerUtils.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { Response } from 'express'; - -import { isEmptyObject } from './parserUtils.js'; - -/** - * @description initial checks for an empty of malformed request object - * @param obj - * @param res - */ -export const failEmptyObjects = (obj: object, res: Response): boolean => { - try { - if (isEmptyObject(obj)) { - res.status(400).send('No object found in request'); - return true; - } - } catch (error) { - res.status(400).send(error); - return true; - } - return false; -}; - -/** - * @description initial checks for an empty of malformed request object - * @param obj - * @param res - */ -export const failIsNotArray = (obj: object, res: Response): boolean => { - try { - if (!Array.isArray(obj)) { - res.status(400).send('No array found in request'); - return true; - } - } catch (error) { - res.status(400).send(error); - return true; - } - return false; -}; diff --git a/e2e/tests/000-upload-showfile.spec.ts b/e2e/tests/000-upload-showfile.spec.ts index 493313bbd..18d50b6a6 100644 --- a/e2e/tests/000-upload-showfile.spec.ts +++ b/e2e/tests/000-upload-showfile.spec.ts @@ -1,9 +1,10 @@ import { test, expect } from '@playwright/test'; +import { randomUUID } from 'crypto'; -import { readFile } from 'fs/promises'; +import { readFile, unlink } from 'fs/promises'; const fileToUpload = 'e2e/tests/fixtures/e2e-test-db.json'; -const fileToDownload = 'e2e/tests/fixtures/tmp/e2e-test-db.json'; +const fileToDownload = 'e2e/tests/fixtures/tmp/'; test('project file upload', async ({ page }) => { await page.goto('http://localhost:4001/editor'); @@ -35,30 +36,30 @@ test('project file upload', async ({ page }) => { await expect(thirdTitle).toHaveValue('Lithuania'); }); +//TODO: this works when testing locally, but not in github actions test.fixme('project file download', async ({ page }) => { - await page.goto('http://localhost:4001/editor'); - - await page.getByRole('button', { name: 'toggle settings' }).click(); - await page.getByRole('button', { name: 'Manage projects' }).click(); - - // workaround to download - // https://playwright.dev/docs/api/class-download - const downloadPromise = page.waitForEvent('download'); + await page.goto('http://localhost:4001/editor/?settings=project__manage'); await page .getByRole('row', { name: /.*currently loaded/i }) .getByLabel('Options') .click(); + // workaround to download + // https://playwright.dev/docs/api/class-download + const downloadPromise = page.waitForEvent('download', { timeout: 10_000 }); await page.getByRole('menuitem', { name: 'Download' }).click(); const download = await downloadPromise; // Wait for the download process to complete and save the downloaded file somewhere. - await download.saveAs(fileToDownload); + const uniqFileToDownload = fileToDownload + randomUUID() + '.json'; + await download.saveAs(uniqFileToDownload); expect(download.failure()).toMatchObject({}); const original = JSON.parse(await readFile(fileToUpload, { encoding: 'utf-8' })); - const fromServer = JSON.parse(await readFile(fileToDownload, { encoding: 'utf-8' })); + const fromServer = JSON.parse(await readFile(uniqFileToDownload, { encoding: 'utf-8' })); + + await unlink(uniqFileToDownload); // when a file is parsed, the server will write the version number to the project file original.settings.version = 'not-important'; diff --git a/e2e/tests/fixtures/e2e-test-db.json b/e2e/tests/fixtures/e2e-test-db.json index ee0c3ed55..e483047b9 100644 --- a/e2e/tests/fixtures/e2e-test-db.json +++ b/e2e/tests/fixtures/e2e-test-db.json @@ -21,6 +21,24 @@ "bab4a", "d3eb1" ], + "flatOrder": [ + "32d31", + "21cd2", + "0b371", + "3cd28", + "e457f", + "01e85", + "1c420", + "b7737", + "d3a80", + "8276c", + "2340b", + "cb90b", + "503c4", + "5e965", + "bab4a", + "d3eb1" + ], "entries": { "32d31": { "type": "event", @@ -469,7 +487,8 @@ "publicInfo": "Rehearsal Schedule - Turin 2022", "backstageUrl": "www.github.com/cpvalente/ontime", "backstageInfo": "Rehearsal Schedule - Turin 2022\nAll performers to wear full costumes for 1st rehearsal", - "projectLogo": null + "projectLogo": null, + "custom": [] }, "settings": { "app": "ontime", @@ -489,12 +508,12 @@ "warningColor": "#FFAB33" }, "customFields": { - "song": { + "Song": { "label": "Song", "type": "string", "colour": "#339E4E" }, - "artist": { + "Artist": { "label": "Artist", "type": "string", "colour": "#3E75E8" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 84da81d6e..634b1c35f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -289,26 +289,26 @@ importers: specifier: ^5.0.5 version: 5.0.5(encoding@0.1.13) cookie: - specifier: ^1.0.2 + specifier: 1.0.2 version: 1.0.2 cookie-parser: - specifier: ^1.4.7 + specifier: 1.4.7 version: 1.4.7 cors: - specifier: ^2.8.5 + specifier: 2.8.5 version: 2.8.5 dotenv: specifier: ^16.0.1 version: 16.3.1 express: - specifier: ^4.21.1 - version: 4.21.2 + specifier: 5.1.0 + version: 5.1.0 express-static-gzip: - specifier: ^2.2.0 - version: 2.2.0 + specifier: 3.0.0 + version: 3.0.0 express-validator: - specifier: ^7.2.0 - version: 7.2.0 + specifier: 7.2.1 + version: 7.2.1 fast-equals: specifier: ^5.0.1 version: 5.0.1 @@ -322,8 +322,8 @@ importers: specifier: ^7.0.1 version: 7.0.1 multer: - specifier: ^1.4.5-lts.1 - version: 1.4.5-lts.1 + specifier: 2.0.1 + version: 2.0.1 ontime-utils: specifier: workspace:* version: link:../../packages/utils @@ -344,17 +344,17 @@ importers: version: 0.18.5 devDependencies: '@types/cookie-parser': - specifier: ^1.4.8 - version: 1.4.8(@types/express@4.17.17) + specifier: 1.4.9 + version: 1.4.9(@types/express@5.0.3) '@types/cors': - specifier: ^2.8.17 - version: 2.8.17 + specifier: 2.8.19 + version: 2.8.19 '@types/express': - specifier: ^4.17.17 - version: 4.17.17 + specifier: 5.0.3 + version: 5.0.3 '@types/multer': - specifier: ^1.4.11 - version: 1.4.11 + specifier: 1.4.13 + version: 1.4.13 '@types/node': specifier: 'catalog:' version: 22.15.26 @@ -2207,13 +2207,13 @@ packages: '@types/connect@3.4.35': resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} - '@types/cookie-parser@1.4.8': - resolution: {integrity: sha512-l37JqFrOJ9yQfRQkljb41l0xVphc7kg5JTjjr+pLRZ0IyZ49V4BQ8vbF4Ut2C2e+WH4al3xD3ZwYwIUfnbT4NQ==} + '@types/cookie-parser@1.4.9': + resolution: {integrity: sha512-tGZiZ2Gtc4m3wIdLkZ8mkj1T6CEHb35+VApbL2T14Dew8HA7c+04dmKqsKRNC+8RJPm16JEK0tFSwdZqubfc4g==} peerDependencies: '@types/express': '*' - '@types/cors@2.8.17': - resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==} + '@types/cors@2.8.19': + resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -2227,11 +2227,11 @@ packages: '@types/estree@1.0.7': resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} - '@types/express-serve-static-core@4.17.33': - resolution: {integrity: sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==} + '@types/express-serve-static-core@5.0.6': + resolution: {integrity: sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==} - '@types/express@4.17.17': - resolution: {integrity: sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==} + '@types/express@5.0.3': + resolution: {integrity: sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==} '@types/fs-extra@9.0.13': resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} @@ -2251,14 +2251,17 @@ packages: '@types/lodash@4.14.191': resolution: {integrity: sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==} + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + '@types/mime@3.0.1': resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==} '@types/ms@0.7.34': resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} - '@types/multer@1.4.11': - resolution: {integrity: sha512-svK240gr6LVWvv3YGyhLlA+6LRRWA4mnGIU7RcNmgjBYFl6665wcXrRfxGp5tEPVHUNm5FMcmq7too9bxCwX/w==} + '@types/multer@1.4.13': + resolution: {integrity: sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcaw==} '@types/node@22.15.26': resolution: {integrity: sha512-lgISkNrqdQ5DAzjBhnDNGKDuXDNo7/1V4FhNzsKREhWLZTOELQAptuAnJMzHtUl1qyEBBy9lNBKQ9WjyiSloTw==} @@ -2296,6 +2299,9 @@ packages: '@types/semver@7.5.5': resolution: {integrity: sha512-+d+WYC1BxJ6yVOgUgzK8gWvp5qF8ssV5r4nsDcZWKRWcDQLQ619tvWAxJQYGgBrO1MnLJC7a5GtiYsAoQ47dJg==} + '@types/send@0.17.5': + resolution: {integrity: sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==} + '@types/serve-static@1.15.0': resolution: {integrity: sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==} @@ -2451,8 +2457,8 @@ packages: abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} - accepts@1.3.8: - resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} acorn-globals@7.0.1: @@ -2571,9 +2577,6 @@ packages: resolution: {integrity: sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==} engines: {node: '>=10'} - array-flatten@1.1.1: - resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} - array-includes@3.1.6: resolution: {integrity: sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==} engines: {node: '>= 0.4'} @@ -2651,9 +2654,9 @@ packages: bluebird@3.7.2: resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} - body-parser@1.20.3: - resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + body-parser@2.2.0: + resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} + engines: {node: '>=18'} boolean@3.2.0: resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} @@ -2741,10 +2744,6 @@ packages: resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==} engines: {node: '>=8'} - call-bind-apply-helpers@1.0.1: - resolution: {integrity: sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==} - engines: {node: '>= 0.4'} - call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -2881,9 +2880,9 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - concat-stream@1.6.2: - resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} - engines: {'0': node >= 0.8} + concat-stream@2.0.0: + resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} + engines: {'0': node >= 6.0} config-file-ts@0.2.4: resolution: {integrity: sha512-cKSW0BfrSaAUnxpgvpXPLaaW/umg4bqg4k3GO1JqlRfpx+d5W0GDXznCMkWotJQek5Mmz1MJVChQnz3IVaeMZQ==} @@ -2891,8 +2890,8 @@ packages: config-file-ts@0.2.8-rc1: resolution: {integrity: sha512-GtNECbVI82bT4RiDIzBSVuTKoSHufnU7Ce7/42bkWZJZFLjmDF2WBpVsvRkhKCfKBnTBb3qZrBwPpFBU/Myvhg==} - content-disposition@0.5.4: - resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + content-disposition@1.0.0: + resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} engines: {node: '>= 0.6'} content-type@1.0.5: @@ -2912,9 +2911,9 @@ packages: cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} - cookie@0.7.1: - resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} - engines: {node: '>= 0.6'} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} cookie@0.7.2: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} @@ -2998,14 +2997,6 @@ packages: resolution: {integrity: sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==} engines: {node: '>=12'} - debug@2.6.9: - resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - debug@4.3.7: resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} engines: {node: '>=6.0'} @@ -3065,10 +3056,6 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} - destroy@1.2.0: - resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - detect-libc@2.0.4: resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} engines: {node: '>=8'} @@ -3182,10 +3169,6 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - encodeurl@1.0.2: - resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} - engines: {node: '>= 0.8'} - encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} @@ -3225,10 +3208,6 @@ packages: es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} - es-object-atoms@1.0.0: - resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} - engines: {node: '>= 0.4'} - es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -3410,16 +3389,16 @@ packages: exponential-backoff@3.1.2: resolution: {integrity: sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==} - express-static-gzip@2.2.0: - resolution: {integrity: sha512-4ZQ0pHX0CAauxmzry2/8XFLM6aZA4NBvg9QezSlsEO1zLnl7vMFa48/WIcjzdfOiEUS4S1npPPKP2NHHYAp6qg==} + express-static-gzip@3.0.0: + resolution: {integrity: sha512-36O10S0asHl3QojOBQQ0ZjXNtElmhgPS6erSUCCZymXkB/CK1mnGqOj4BTJN+FYRDIzVFnzo3wLFCZJvAk6rQQ==} - express-validator@7.2.0: - resolution: {integrity: sha512-I2ByKD8panjtr8Y05l21Wph9xk7kk64UMyvJCl/fFM/3CTJq8isXYPLeKW/aZBCdb/LYNv63PwhY8khw8VWocA==} + express-validator@7.2.1: + resolution: {integrity: sha512-CjNE6aakfpuwGaHQZ3m8ltCG2Qvivd7RHtVMS/6nVxOM7xVGqr4bhflsm4+N5FP5zI7Zxp+Hae+9RE+o8e3ZOQ==} engines: {node: '>= 8.0.0'} - express@4.21.2: - resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} - engines: {node: '>= 0.10.0'} + express@5.1.0: + resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} + engines: {node: '>= 18'} extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -3478,8 +3457,8 @@ packages: resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} engines: {node: '>=8'} - finalhandler@1.3.1: - resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} + finalhandler@2.1.0: + resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} engines: {node: '>= 0.8'} find-root@1.1.0: @@ -3546,9 +3525,9 @@ packages: framesync@6.1.2: resolution: {integrity: sha512-jBTqhX6KaQVDyus8muwZbBeGGP0XgujBRbQ7gM7BRdS3CadCZIHiawyzYLnafYcvZIh5j8WE7cxZKFn7dXhu9g==} - fresh@0.5.2: - resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} - engines: {node: '>= 0.6'} + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} @@ -3615,10 +3594,6 @@ packages: get-intrinsic@1.2.2: resolution: {integrity: sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==} - get-intrinsic@1.2.6: - resolution: {integrity: sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==} - engines: {node: '>= 0.4'} - get-intrinsic@1.2.7: resolution: {integrity: sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==} engines: {node: '>= 0.4'} @@ -3827,10 +3802,6 @@ packages: engines: {node: ^8.11.2 || >=10} os: [darwin] - iconv-lite@0.4.24: - resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} - engines: {node: '>=0.10.0'} - iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} @@ -3959,6 +3930,9 @@ packages: is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-regex@1.1.4: resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} engines: {node: '>= 0.4'} @@ -4198,17 +4172,18 @@ packages: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} - merge-descriptors@1.0.3: - resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - methods@1.1.2: - resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} - engines: {node: '>= 0.6'} - micromatch@4.0.5: resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} engines: {node: '>=8.6'} @@ -4217,14 +4192,17 @@ packages: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} - mime@1.6.0: - resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} - engines: {node: '>=4'} - hasBin: true + mime-types@3.0.1: + resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} + engines: {node: '>= 0.6'} mime@2.6.0: resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} @@ -4322,15 +4300,12 @@ packages: engines: {node: '>=10'} hasBin: true - ms@2.0.0: - resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} - ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - multer@1.4.5-lts.1: - resolution: {integrity: sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==} - engines: {node: '>= 6.0.0'} + multer@2.0.1: + resolution: {integrity: sha512-Ug8bXeTIUlxurg8xLTEskKShvcKDZALo1THEX5E41pYCD2sCVub5/kIRIGqWNoqV6szyLyQKV6mD4QUrWE5GCQ==} + engines: {node: '>= 10.16.0'} nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} @@ -4349,6 +4324,10 @@ packages: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + no-case@3.0.4: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} @@ -4519,8 +4498,9 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} - path-to-regexp@0.1.12: - resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + path-to-regexp@8.2.0: + resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} + engines: {node: '>=16'} path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} @@ -4639,8 +4619,8 @@ packages: resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} engines: {node: '>=0.6'} - qs@6.13.0: - resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} engines: {node: '>=0.6'} querystringify@2.2.0: @@ -4657,8 +4637,8 @@ packages: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} - raw-body@2.5.2: - resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + raw-body@3.0.0: + resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} engines: {node: '>= 0.8'} react-clientside-effect@1.2.6: @@ -4867,6 +4847,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -4919,17 +4903,17 @@ packages: engines: {node: '>=10'} hasBin: true - send@0.19.0: - resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} - engines: {node: '>= 0.8.0'} + send@1.2.0: + resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} + engines: {node: '>= 18'} serialize-error@7.0.1: resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} engines: {node: '>=10'} - serve-static@1.16.2: - resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} - engines: {node: '>= 0.8.0'} + serve-static@2.2.0: + resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} + engines: {node: '>= 18'} server-timing@3.3.3: resolution: {integrity: sha512-TP0xWAca4oM8H/PSdeaGgp2qm+HrZ2cWCRcMXS2t500a7Wum/hSojlpTW43VZsIUSVNlKPFGDknH34IqF+mbBg==} @@ -5313,6 +5297,10 @@ packages: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + typed-array-length@1.0.4: resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} @@ -5414,10 +5402,6 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - utils-merge@1.0.1: - resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} - engines: {node: '>= 0.4.0'} - uuid@9.0.1: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true @@ -7600,11 +7584,11 @@ snapshots: dependencies: '@types/node': 22.15.26 - '@types/cookie-parser@1.4.8(@types/express@4.17.17)': + '@types/cookie-parser@1.4.9(@types/express@5.0.3)': dependencies: - '@types/express': 4.17.17 + '@types/express': 5.0.3 - '@types/cors@2.8.17': + '@types/cors@2.8.19': dependencies: '@types/node': 22.15.26 @@ -7618,17 +7602,17 @@ snapshots: '@types/estree@1.0.7': {} - '@types/express-serve-static-core@4.17.33': + '@types/express-serve-static-core@5.0.6': dependencies: '@types/node': 22.15.26 '@types/qs': 6.9.7 '@types/range-parser': 1.2.4 + '@types/send': 0.17.5 - '@types/express@4.17.17': + '@types/express@5.0.3': dependencies: '@types/body-parser': 1.19.2 - '@types/express-serve-static-core': 4.17.33 - '@types/qs': 6.9.7 + '@types/express-serve-static-core': 5.0.6 '@types/serve-static': 1.15.0 '@types/fs-extra@9.0.13': @@ -7649,13 +7633,15 @@ snapshots: '@types/lodash@4.14.191': {} + '@types/mime@1.3.5': {} + '@types/mime@3.0.1': {} '@types/ms@0.7.34': {} - '@types/multer@1.4.11': + '@types/multer@1.4.13': dependencies: - '@types/express': 4.17.17 + '@types/express': 5.0.3 '@types/node@22.15.26': dependencies: @@ -7695,6 +7681,11 @@ snapshots: '@types/semver@7.5.5': {} + '@types/send@0.17.5': + dependencies: + '@types/mime': 1.3.5 + '@types/node': 22.15.26 + '@types/serve-static@1.15.0': dependencies: '@types/mime': 3.0.1 @@ -7904,10 +7895,10 @@ snapshots: abbrev@1.1.1: {} - accepts@1.3.8: + accepts@2.0.0: dependencies: - mime-types: 2.1.35 - negotiator: 0.6.3 + mime-types: 3.0.1 + negotiator: 1.0.0 acorn-globals@7.0.1: dependencies: @@ -8104,8 +8095,6 @@ snapshots: dependencies: tslib: 2.6.2 - array-flatten@1.1.1: {} - array-includes@3.1.6: dependencies: call-bind: 1.0.2 @@ -8185,20 +8174,17 @@ snapshots: bluebird@3.7.2: {} - body-parser@1.20.3: + body-parser@2.2.0: dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 + debug: 4.4.1 http-errors: 2.0.0 - iconv-lite: 0.4.24 + iconv-lite: 0.6.3 on-finished: 2.4.1 - qs: 6.13.0 - raw-body: 2.5.2 - type-is: 1.6.18 - unpipe: 1.0.0 + qs: 6.14.0 + raw-body: 3.0.0 + type-is: 2.0.1 transitivePeerDependencies: - supports-color @@ -8357,11 +8343,6 @@ snapshots: normalize-url: 6.1.0 responselike: 2.0.1 - call-bind-apply-helpers@1.0.1: - dependencies: - es-errors: 1.3.0 - function-bind: 1.1.2 - call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -8374,7 +8355,7 @@ snapshots: call-bound@1.0.3: dependencies: - call-bind-apply-helpers: 1.0.1 + call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.2.7 callsites@3.1.0: {} @@ -8500,11 +8481,11 @@ snapshots: concat-map@0.0.1: {} - concat-stream@1.6.2: + concat-stream@2.0.0: dependencies: buffer-from: 1.1.2 inherits: 2.0.4 - readable-stream: 2.3.8 + readable-stream: 3.6.2 typedarray: 0.0.6 config-file-ts@0.2.4: @@ -8517,7 +8498,7 @@ snapshots: glob: 10.4.5 typescript: 5.5.3 - content-disposition@0.5.4: + content-disposition@1.0.0: dependencies: safe-buffer: 5.2.1 @@ -8534,7 +8515,7 @@ snapshots: cookie-signature@1.0.6: {} - cookie@0.7.1: {} + cookie-signature@1.2.2: {} cookie@0.7.2: {} @@ -8625,10 +8606,6 @@ snapshots: whatwg-url: 11.0.0 optional: true - debug@2.6.9: - dependencies: - ms: 2.0.0 - debug@4.3.7: dependencies: ms: 2.1.3 @@ -8672,8 +8649,6 @@ snapshots: depd@2.0.0: {} - destroy@1.2.0: {} - detect-libc@2.0.4: {} detect-node-es@1.1.0: {} @@ -8838,8 +8813,6 @@ snapshots: emoji-regex@9.2.2: {} - encodeurl@1.0.2: {} - encodeurl@2.0.0: {} encoding@0.1.13: @@ -8903,10 +8876,6 @@ snapshots: es-module-lexer@1.7.0: {} - es-object-atoms@1.0.0: - dependencies: - es-errors: 1.3.0 - es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -9179,50 +9148,47 @@ snapshots: exponential-backoff@3.1.2: {} - express-static-gzip@2.2.0: + express-static-gzip@3.0.0: dependencies: + mime-types: 3.0.1 parseurl: 1.3.3 - serve-static: 1.16.2 + serve-static: 2.2.0 transitivePeerDependencies: - supports-color - express-validator@7.2.0: + express-validator@7.2.1: dependencies: lodash: 4.17.21 validator: 13.12.0 - express@4.21.2: + express@5.1.0: dependencies: - accepts: 1.3.8 - array-flatten: 1.1.1 - body-parser: 1.20.3 - content-disposition: 0.5.4 + accepts: 2.0.0 + body-parser: 2.2.0 + content-disposition: 1.0.0 content-type: 1.0.5 - cookie: 0.7.1 - cookie-signature: 1.0.6 - debug: 2.6.9 - depd: 2.0.0 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.1 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 - finalhandler: 1.3.1 - fresh: 0.5.2 + finalhandler: 2.1.0 + fresh: 2.0.0 http-errors: 2.0.0 - merge-descriptors: 1.0.3 - methods: 1.1.2 + merge-descriptors: 2.0.0 + mime-types: 3.0.1 on-finished: 2.4.1 + once: 1.4.0 parseurl: 1.3.3 - path-to-regexp: 0.1.12 proxy-addr: 2.0.7 - qs: 6.13.0 + qs: 6.14.0 range-parser: 1.2.1 - safe-buffer: 5.2.1 - send: 0.19.0 - serve-static: 1.16.2 - setprototypeof: 1.2.0 + router: 2.2.0 + send: 1.2.0 + serve-static: 2.2.0 statuses: 2.0.1 - type-is: 1.6.18 - utils-merge: 1.0.1 + type-is: 2.0.1 vary: 1.1.2 transitivePeerDependencies: - supports-color @@ -9284,15 +9250,14 @@ snapshots: dependencies: to-regex-range: 5.0.1 - finalhandler@1.3.1: + finalhandler@2.1.0: dependencies: - debug: 2.6.9 + debug: 4.4.1 encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 parseurl: 1.3.3 statuses: 2.0.1 - unpipe: 1.0.0 transitivePeerDependencies: - supports-color @@ -9351,7 +9316,7 @@ snapshots: dependencies: tslib: 2.4.0 - fresh@0.5.2: {} + fresh@2.0.0: {} fs-constants@1.0.0: {} @@ -9432,19 +9397,6 @@ snapshots: has-symbols: 1.0.3 hasown: 2.0.0 - get-intrinsic@1.2.6: - dependencies: - call-bind-apply-helpers: 1.0.1 - dunder-proto: 1.0.1 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.0.0 - function-bind: 1.1.2 - gopd: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.2 - math-intrinsics: 1.1.0 - get-intrinsic@1.2.7: dependencies: call-bind-apply-helpers: 1.0.2 @@ -9738,10 +9690,6 @@ snapshots: node-addon-api: 1.7.2 optional: true - iconv-lite@0.4.24: - dependencies: - safer-buffer: 2.1.2 - iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 @@ -9851,6 +9799,8 @@ snapshots: is-potential-custom-element-name@1.0.1: optional: true + is-promise@4.0.0: {} + is-regex@1.1.4: dependencies: call-bind: 1.0.2 @@ -10117,12 +10067,12 @@ snapshots: media-typer@0.3.0: {} - merge-descriptors@1.0.3: {} + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} merge2@1.4.1: {} - methods@1.1.2: {} - micromatch@4.0.5: dependencies: braces: 3.0.2 @@ -10130,11 +10080,15 @@ snapshots: mime-db@1.52.0: {} + mime-db@1.54.0: {} + mime-types@2.1.35: dependencies: mime-db: 1.52.0 - mime@1.6.0: {} + mime-types@3.0.1: + dependencies: + mime-db: 1.54.0 mime@2.6.0: {} @@ -10215,15 +10169,13 @@ snapshots: mkdirp@1.0.4: {} - ms@2.0.0: {} - ms@2.1.3: {} - multer@1.4.5-lts.1: + multer@2.0.1: dependencies: append-field: 1.0.0 busboy: 1.6.0 - concat-stream: 1.6.2 + concat-stream: 2.0.0 mkdirp: 0.5.6 object-assign: 4.1.1 type-is: 1.6.18 @@ -10237,6 +10189,8 @@ snapshots: negotiator@0.6.3: {} + negotiator@1.0.0: {} + no-case@3.0.4: dependencies: lower-case: 2.0.2 @@ -10405,7 +10359,7 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 - path-to-regexp@0.1.12: {} + path-to-regexp@8.2.0: {} path-type@4.0.0: {} @@ -10499,7 +10453,7 @@ snapshots: dependencies: side-channel: 1.0.4 - qs@6.13.0: + qs@6.14.0: dependencies: side-channel: 1.1.0 @@ -10512,11 +10466,11 @@ snapshots: range-parser@1.2.1: {} - raw-body@2.5.2: + raw-body@3.0.0: dependencies: bytes: 3.1.2 http-errors: 2.0.0 - iconv-lite: 0.4.24 + iconv-lite: 0.6.3 unpipe: 1.0.0 react-clientside-effect@1.2.6(react@18.3.1): @@ -10753,6 +10707,16 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.41.1 fsevents: 2.3.3 + router@2.2.0: + dependencies: + debug: 4.4.1 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.2.0 + transitivePeerDependencies: + - supports-color + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -10803,17 +10767,15 @@ snapshots: semver@7.6.2: {} - send@0.19.0: + send@1.2.0: dependencies: - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 - encodeurl: 1.0.2 + debug: 4.4.1 + encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 - fresh: 0.5.2 + fresh: 2.0.0 http-errors: 2.0.0 - mime: 1.6.0 + mime-types: 3.0.1 ms: 2.1.3 on-finished: 2.4.1 range-parser: 1.2.1 @@ -10826,12 +10788,12 @@ snapshots: type-fest: 0.13.1 optional: true - serve-static@1.16.2: + serve-static@2.2.0: dependencies: encodeurl: 2.0.0 escape-html: 1.0.3 parseurl: 1.3.3 - send: 0.19.0 + send: 1.2.0 transitivePeerDependencies: - supports-color @@ -10868,14 +10830,14 @@ snapshots: dependencies: call-bound: 1.0.3 es-errors: 1.3.0 - get-intrinsic: 1.2.6 + get-intrinsic: 1.2.7 object-inspect: 1.13.3 side-channel-weakmap@1.0.2: dependencies: call-bound: 1.0.3 es-errors: 1.3.0 - get-intrinsic: 1.2.6 + get-intrinsic: 1.2.7 object-inspect: 1.13.3 side-channel-map: 1.0.1 @@ -11218,6 +11180,12 @@ snapshots: media-typer: 0.3.0 mime-types: 2.1.35 + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.1 + typed-array-length@1.0.4: dependencies: call-bind: 1.0.2 @@ -11311,8 +11279,6 @@ snapshots: util-deprecate@1.0.2: {} - utils-merge@1.0.1: {} - uuid@9.0.1: {} validator@13.12.0: {}