Validate project filename (#1046)

This commit is contained in:
Alex Christoffer Rasmussen
2024-06-06 22:37:41 +02:00
committed by GitHub
parent edb48a2fc7
commit c2fa115946
7 changed files with 174 additions and 107 deletions
+3 -3
View File
@@ -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,
+1
View File
@@ -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"
+8 -21
View File
@@ -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 });
+11 -14
View File
@@ -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);
+28 -60
View File
@@ -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);
+121 -8
View File
@@ -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'}