mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-12 19:03:47 +00:00
Validate project filename (#1046)
This commit is contained in:
committed by
GitHub
parent
edb48a2fc7
commit
c2fa115946
@@ -11,8 +11,8 @@ const dbPath = `${apiEntryUrl}/db`;
|
||||
/**
|
||||
* HTTP request to the current DB
|
||||
*/
|
||||
async function getDb(fileName?: string): Promise<AxiosResponse<DatabaseModel>> {
|
||||
return axios.post(`${dbPath}/download/`, { fileName });
|
||||
async function getDb(filename: string): Promise<AxiosResponse<DatabaseModel>> {
|
||||
return axios.post(`${dbPath}/download/`, { filename });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -123,7 +123,7 @@ export async function renameProject(filename: string, newFilename: string): Prom
|
||||
const url = `${dbPath}/${filename}/rename`;
|
||||
const decodedUrl = decodeURIComponent(url);
|
||||
const res = await axios.put(decodedUrl, {
|
||||
newFilename,
|
||||
filename: newFilename,
|
||||
});
|
||||
return res.data;
|
||||
}
|
||||
|
||||
@@ -50,7 +50,8 @@ export default function ProjectCreateForm(props: ProjectCreateFromProps) {
|
||||
const handleSubmitCreate = async (values: ProjectCreateFormValues) => {
|
||||
try {
|
||||
setError(null);
|
||||
const filename = values.title?.trim();
|
||||
|
||||
const filename = values.title ?? 'untitled';
|
||||
|
||||
await createProject({
|
||||
...values,
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"node-osc": "^9.0.2",
|
||||
"node-xlsx": "^0.23.0",
|
||||
"ontime-utils": "workspace:*",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"steno": "^3.1.0",
|
||||
"ts-essentials": "^9.4.1",
|
||||
"ws": "^8.13.0"
|
||||
|
||||
@@ -16,7 +16,6 @@ import { failEmptyObjects } from '../../utils/routerUtils.js';
|
||||
import { resolveDbDirectory, resolveProjectsDirectory } from '../../setup/index.js';
|
||||
|
||||
import * as projectService from '../../services/project-service/ProjectService.js';
|
||||
import { ensureJsonExtension } from '../../utils/fileManagement.js';
|
||||
import { generateUniqueFileName } from '../../utils/generateUniqueFilename.js';
|
||||
import { appStateService } from '../../services/app-state-service/AppStateService.js';
|
||||
import { oscIntegration } from '../../services/integration-service/OscIntegration.js';
|
||||
@@ -54,8 +53,7 @@ export async function patchPartialProjectFile(req: Request, res: Response<Databa
|
||||
*/
|
||||
export async function createProjectFile(req: Request, res: Response<{ filename: string } | ErrorResponse>) {
|
||||
try {
|
||||
const originalFilename = ensureJsonExtension(req.body.title || 'Untitled');
|
||||
const filename = generateUniqueFileName(resolveProjectsDirectory, originalFilename);
|
||||
const filename = generateUniqueFileName(resolveProjectsDirectory, req.body.filename);
|
||||
const errors = projectService.validateProjectFiles({ newFilename: filename });
|
||||
|
||||
if (errors.length) {
|
||||
@@ -71,7 +69,7 @@ export async function createProjectFile(req: Request, res: Response<{ filename:
|
||||
backstageInfo: req.body?.backstageInfo ?? '',
|
||||
};
|
||||
|
||||
projectService.createProjectFile(filename, newProjectData);
|
||||
await projectService.createProjectFile(filename, newProjectData);
|
||||
|
||||
res.status(200).send({
|
||||
filename,
|
||||
@@ -83,29 +81,18 @@ export async function createProjectFile(req: Request, res: Response<{ filename:
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function finds the correct project file to download
|
||||
*/
|
||||
function selectProjectFile(fileName?: string) {
|
||||
const projectsDirectory = resolveDbDirectory;
|
||||
const fileToDownload = fileName ? ensureJsonExtension(fileName) : projectService.getProjectTitle();
|
||||
const pathToFile = join(projectsDirectory, fileToDownload);
|
||||
|
||||
return { pathToFile, name: fileToDownload };
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows downloading of a optionally given project files
|
||||
* If no {filename} is provided, loaded file will be served
|
||||
* Allows downloading of project files
|
||||
*/
|
||||
export async function projectDownload(req: Request, res: Response) {
|
||||
const { pathToFile, name } = selectProjectFile(req.body?.fileName);
|
||||
const { filename } = req.body;
|
||||
const pathToFile = join(resolveDbDirectory, filename);
|
||||
|
||||
// Check if the file exists before attempting to download
|
||||
if (!existsSync(pathToFile)) {
|
||||
return res.status(404).send({ message: `Project ${name} not found.` });
|
||||
return res.status(404).send({ message: `Project ${filename} not found.` });
|
||||
}
|
||||
|
||||
res.download(pathToFile, name, (error) => {
|
||||
res.download(pathToFile, filename, (error) => {
|
||||
if (error) {
|
||||
const message = getErrorMessage(error);
|
||||
res.status(500).send({ message });
|
||||
@@ -228,7 +215,7 @@ export async function duplicateProjectFile(req: Request, res: Response<MessageRe
|
||||
*/
|
||||
export async function renameProjectFile(req: Request, res: Response<MessageResponse | ErrorResponse>) {
|
||||
try {
|
||||
const { newFilename } = req.body;
|
||||
const { filename: newFilename } = req.body;
|
||||
const { filename } = req.params;
|
||||
|
||||
const errors = projectService.validateProjectFiles({ filename, newFilename });
|
||||
|
||||
@@ -14,28 +14,25 @@ import {
|
||||
} from './db.controller.js';
|
||||
import { uploadProjectFile } from './db.middleware.js';
|
||||
import {
|
||||
projectSanitiser,
|
||||
sanitizeProjectFilename,
|
||||
validateDownloadProject,
|
||||
validateLoadProjectFile,
|
||||
validatePatchProjectFile,
|
||||
validateProjectDuplicate,
|
||||
validateProjectRename,
|
||||
validateNewProject,
|
||||
validatePatchProject,
|
||||
validateFilenameBody,
|
||||
validateFilenameParam,
|
||||
} from './db.validation.js';
|
||||
|
||||
export const router = express.Router();
|
||||
|
||||
router.post('/download', validateDownloadProject, projectDownload);
|
||||
router.post('/download', validateFilenameBody, projectDownload);
|
||||
router.post('/upload', uploadProjectFile, postProjectFile);
|
||||
|
||||
router.patch('/', validatePatchProjectFile, patchPartialProjectFile);
|
||||
router.post('/new', projectSanitiser, createProjectFile);
|
||||
router.patch('/', validatePatchProject, patchPartialProjectFile);
|
||||
router.post('/new', validateFilenameBody, validateNewProject, createProjectFile);
|
||||
|
||||
router.get('/all', listProjects);
|
||||
|
||||
router.post('/load', validateLoadProjectFile, sanitizeProjectFilename, loadProject);
|
||||
router.post('/:filename/duplicate', validateProjectDuplicate, sanitizeProjectFilename, duplicateProjectFile);
|
||||
router.put('/:filename/rename', validateProjectRename, sanitizeProjectFilename, renameProjectFile);
|
||||
router.delete('/:filename', sanitizeProjectFilename, deleteProjectFile);
|
||||
router.post('/load', validateFilenameBody, loadProject);
|
||||
router.post('/:filename/duplicate', validateFilenameParam, validateFilenameBody, duplicateProjectFile);
|
||||
router.put('/:filename/rename', validateFilenameParam, validateFilenameBody, renameProjectFile);
|
||||
router.delete('/:filename', validateFilenameParam, deleteProjectFile);
|
||||
|
||||
router.get('/info', getInfo);
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { body, validationResult } from 'express-validator';
|
||||
|
||||
import { body, param, validationResult } from 'express-validator';
|
||||
import { ensureJsonExtension } from '../../utils/fileManagement.js';
|
||||
import sanitize from 'sanitize-filename';
|
||||
|
||||
export const projectSanitiser = [
|
||||
/**
|
||||
* @description Validates request for a new project.
|
||||
*/
|
||||
export const validateNewProject = [
|
||||
body('title').optional().isString().trim(),
|
||||
body('description').optional().isString().trim(),
|
||||
body('publicUrl').optional().isString().trim(),
|
||||
@@ -19,18 +22,10 @@ export const projectSanitiser = [
|
||||
},
|
||||
];
|
||||
|
||||
export const sanitizeProjectFilename = (req: Request, _res: Response, next: NextFunction) => {
|
||||
const { filename, newFilename } = req.body;
|
||||
const { filename: projectName } = req.params;
|
||||
|
||||
req.body.filename = ensureJsonExtension(filename);
|
||||
req.body.newFilename = ensureJsonExtension(newFilename);
|
||||
req.params.filename = ensureJsonExtension(projectName);
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
export const validatePatchProjectFile = [
|
||||
/**
|
||||
* @description Validates request for pathing data in the project.
|
||||
*/
|
||||
export const validatePatchProject = [
|
||||
body('rundown').isArray().optional({ nullable: false }),
|
||||
body('project').isObject().optional({ nullable: false }),
|
||||
body('settings').isObject().optional({ nullable: false }),
|
||||
@@ -47,31 +42,18 @@ export const validatePatchProjectFile = [
|
||||
];
|
||||
|
||||
/**
|
||||
* @description Validates the filename for loading a project file.
|
||||
* @description Validates request with filename in the body.
|
||||
*/
|
||||
export const validateLoadProjectFile = [
|
||||
body('filename').exists().withMessage('Filename is required').isString().withMessage('Filename must be a string'),
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(422).json({ errors: errors.array() });
|
||||
}
|
||||
next();
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* @description Validates the filenames for duplicating a project.
|
||||
*/
|
||||
export const validateProjectDuplicate = [
|
||||
body('newFilename')
|
||||
export const validateFilenameBody = [
|
||||
body('filename')
|
||||
.exists()
|
||||
.withMessage('New project filename is required')
|
||||
.isString()
|
||||
.withMessage('New project filename must be a string')
|
||||
.isLength({ min: 1, max: 255 })
|
||||
.withMessage('New project filename must be between 1 and 255 characters'),
|
||||
.trim()
|
||||
.customSanitizer((input: string) => sanitize(input))
|
||||
.withMessage('Failed to sanitize the filename')
|
||||
.notEmpty()
|
||||
.withMessage('Filename was empty or contained only invalid characters')
|
||||
.customSanitizer((input: string) => ensureJsonExtension(input)),
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
@@ -84,32 +66,18 @@ export const validateProjectDuplicate = [
|
||||
];
|
||||
|
||||
/**
|
||||
* @description Validates the filenames for renaming a project.
|
||||
* @description Validates request with filename in the params.
|
||||
*/
|
||||
export const validateProjectRename = [
|
||||
body('newFilename')
|
||||
export const validateFilenameParam = [
|
||||
param('filename')
|
||||
.exists()
|
||||
.withMessage('Duplicate project filename is required')
|
||||
.isString()
|
||||
.withMessage('Duplicate project filename must be a string')
|
||||
.isLength({ min: 1, max: 255 })
|
||||
.withMessage('Duplicate project filename must be between 1 and 255 characters'),
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(422).json({ errors: errors.array() });
|
||||
}
|
||||
|
||||
next();
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* @description Validates a download request which can include an optional project name.
|
||||
*/
|
||||
export const validateDownloadProject = [
|
||||
body('fileName').isString().optional(),
|
||||
.trim()
|
||||
.customSanitizer((input: string) => sanitize(input))
|
||||
.withMessage('Failed to sanitize the filename')
|
||||
.notEmpty()
|
||||
.withMessage('Filename was empty or contained only invalid characters')
|
||||
.customSanitizer((input: string) => ensureJsonExtension(input)),
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
|
||||
Generated
+121
-8
@@ -212,7 +212,7 @@ importers:
|
||||
version: 5.4.3
|
||||
vite:
|
||||
specifier: ^5.2.11
|
||||
version: 5.2.11(@types/node@18.11.18)(sass@1.57.1)
|
||||
version: 5.2.11(sass@1.57.1)
|
||||
vite-plugin-compression2:
|
||||
specifier: ^0.12.0
|
||||
version: 0.12.0
|
||||
@@ -224,7 +224,7 @@ importers:
|
||||
version: 4.3.1(typescript@5.4.3)(vite@5.2.11)
|
||||
vitest:
|
||||
specifier: ^1.6.0
|
||||
version: 1.6.0(@types/node@18.11.18)(jsdom@21.1.0)(sass@1.57.1)
|
||||
version: 1.6.0(jsdom@21.1.0)(sass@1.57.1)
|
||||
|
||||
apps/electron:
|
||||
devDependencies:
|
||||
@@ -288,6 +288,9 @@ importers:
|
||||
ontime-utils:
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/utils
|
||||
sanitize-filename:
|
||||
specifier: ^1.6.3
|
||||
version: 1.6.3
|
||||
steno:
|
||||
specifier: ^3.1.0
|
||||
version: 3.2.0
|
||||
@@ -3872,7 +3875,7 @@ packages:
|
||||
'@babel/plugin-transform-react-jsx-source': 7.23.3(@babel/core@7.23.6)
|
||||
'@types/babel__core': 7.20.5
|
||||
react-refresh: 0.14.0
|
||||
vite: 5.2.11(@types/node@18.11.18)(sass@1.57.1)
|
||||
vite: 5.2.11(sass@1.57.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
@@ -8560,7 +8563,6 @@ packages:
|
||||
resolution: {integrity: sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==}
|
||||
dependencies:
|
||||
truncate-utf8-bytes: 1.0.2
|
||||
dev: true
|
||||
|
||||
/sass@1.57.1:
|
||||
resolution: {integrity: sha512-O2+LwLS79op7GI0xZ8fqzF7X2m/m8WFfI02dHOdsK5R2ECeS5F62zrwg/relM1rjSLy7Vd/DiMNIvPrQGsA0jw==}
|
||||
@@ -9146,7 +9148,6 @@ packages:
|
||||
resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==}
|
||||
dependencies:
|
||||
utf8-byte-length: 1.0.4
|
||||
dev: true
|
||||
|
||||
/ts-api-utils@1.0.3(typescript@5.4.3):
|
||||
resolution: {integrity: sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==}
|
||||
@@ -9488,7 +9489,6 @@ packages:
|
||||
|
||||
/utf8-byte-length@1.0.4:
|
||||
resolution: {integrity: sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA==}
|
||||
dev: true
|
||||
|
||||
/util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
@@ -9549,6 +9549,27 @@ packages:
|
||||
- terser
|
||||
dev: true
|
||||
|
||||
/vite-node@1.6.0(sass@1.57.1):
|
||||
resolution: {integrity: sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
debug: 4.3.4
|
||||
pathe: 1.1.1
|
||||
picocolors: 1.0.0
|
||||
vite: 5.2.11(sass@1.57.1)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- less
|
||||
- lightningcss
|
||||
- sass
|
||||
- stylus
|
||||
- sugarss
|
||||
- supports-color
|
||||
- terser
|
||||
dev: true
|
||||
|
||||
/vite-plugin-compression2@0.12.0:
|
||||
resolution: {integrity: sha512-9zdEF9xKVezETSF1l1bHoOk8LNoKIHB+DZVgSIGuGWaYupwFmsAGh0uwRcmK6rVHacxQRBECVYdtfc65DPDRfg==}
|
||||
dependencies:
|
||||
@@ -9566,7 +9587,7 @@ packages:
|
||||
'@rollup/pluginutils': 5.1.0
|
||||
'@svgr/core': 8.1.0(typescript@5.4.3)
|
||||
'@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0)
|
||||
vite: 5.2.11(@types/node@18.11.18)(sass@1.57.1)
|
||||
vite: 5.2.11(sass@1.57.1)
|
||||
transitivePeerDependencies:
|
||||
- rollup
|
||||
- supports-color
|
||||
@@ -9584,7 +9605,7 @@ packages:
|
||||
debug: 4.3.4
|
||||
globrex: 0.1.2
|
||||
tsconfck: 3.0.2(typescript@5.4.3)
|
||||
vite: 5.2.11(@types/node@18.11.18)(sass@1.57.1)
|
||||
vite: 5.2.11(sass@1.57.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
@@ -9627,6 +9648,42 @@ packages:
|
||||
fsevents: 2.3.3
|
||||
dev: true
|
||||
|
||||
/vite@5.2.11(sass@1.57.1):
|
||||
resolution: {integrity: sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@types/node': ^18.0.0 || >=20.0.0
|
||||
less: '*'
|
||||
lightningcss: ^1.21.0
|
||||
sass: '*'
|
||||
stylus: '*'
|
||||
sugarss: '*'
|
||||
terser: ^5.4.0
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
less:
|
||||
optional: true
|
||||
lightningcss:
|
||||
optional: true
|
||||
sass:
|
||||
optional: true
|
||||
stylus:
|
||||
optional: true
|
||||
sugarss:
|
||||
optional: true
|
||||
terser:
|
||||
optional: true
|
||||
dependencies:
|
||||
esbuild: 0.20.2
|
||||
postcss: 8.4.38
|
||||
rollup: 4.17.2
|
||||
sass: 1.57.1
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
dev: true
|
||||
|
||||
/vitest@1.6.0(@types/node@18.11.18)(jsdom@21.1.0)(sass@1.57.1):
|
||||
resolution: {integrity: sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
@@ -9684,6 +9741,62 @@ packages:
|
||||
- terser
|
||||
dev: true
|
||||
|
||||
/vitest@1.6.0(jsdom@21.1.0)(sass@1.57.1):
|
||||
resolution: {integrity: sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@edge-runtime/vm': '*'
|
||||
'@types/node': ^18.0.0 || >=20.0.0
|
||||
'@vitest/browser': 1.6.0
|
||||
'@vitest/ui': 1.6.0
|
||||
happy-dom: '*'
|
||||
jsdom: '*'
|
||||
peerDependenciesMeta:
|
||||
'@edge-runtime/vm':
|
||||
optional: true
|
||||
'@types/node':
|
||||
optional: true
|
||||
'@vitest/browser':
|
||||
optional: true
|
||||
'@vitest/ui':
|
||||
optional: true
|
||||
happy-dom:
|
||||
optional: true
|
||||
jsdom:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@vitest/expect': 1.6.0
|
||||
'@vitest/runner': 1.6.0
|
||||
'@vitest/snapshot': 1.6.0
|
||||
'@vitest/spy': 1.6.0
|
||||
'@vitest/utils': 1.6.0
|
||||
acorn-walk: 8.3.2
|
||||
chai: 4.3.10
|
||||
debug: 4.3.4
|
||||
execa: 8.0.1
|
||||
jsdom: 21.1.0
|
||||
local-pkg: 0.5.0
|
||||
magic-string: 0.30.5
|
||||
pathe: 1.1.1
|
||||
picocolors: 1.0.0
|
||||
std-env: 3.6.0
|
||||
strip-literal: 2.1.0
|
||||
tinybench: 2.5.1
|
||||
tinypool: 0.8.4
|
||||
vite: 5.2.11(sass@1.57.1)
|
||||
vite-node: 1.6.0(sass@1.57.1)
|
||||
why-is-node-running: 2.2.2
|
||||
transitivePeerDependencies:
|
||||
- less
|
||||
- lightningcss
|
||||
- sass
|
||||
- stylus
|
||||
- sugarss
|
||||
- supports-color
|
||||
- terser
|
||||
dev: true
|
||||
|
||||
/w3c-xmlserializer@4.0.0:
|
||||
resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
Reference in New Issue
Block a user