From 166fc87d7c65cdc716c8f8fd8387952bbb98fa4f Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 27 Nov 2023 09:43:52 +0100 Subject: [PATCH] add rest endpoints for creating/deleting images in processes --- src/management-system-v2/lib/openapiSchema.ts | 109 ++++++++++-- .../src/backend/openapi.json | 165 ++++++++++++++---- .../src/backend/server/index.js | 5 +- .../src/backend/server/rest-api/process.js | 85 ++++++++- .../data/fileHandling.js | 45 ++++- .../shared-electron-server/data/process.js | 33 +++- 6 files changed, 386 insertions(+), 56 deletions(-) diff --git a/src/management-system-v2/lib/openapiSchema.ts b/src/management-system-v2/lib/openapiSchema.ts index 6c3a06b54..3858314de 100644 --- a/src/management-system-v2/lib/openapiSchema.ts +++ b/src/management-system-v2/lib/openapiSchema.ts @@ -41,12 +41,21 @@ export interface paths { get: operations['getVersionById']; }; '/process/{definitionId}/images': { - /** @description Get all images used in a process. */ - get: operations['getImageOfProcessById']; + /** @description Get all image filenames used in a process. */ + get: operations['getImageFilenamesByProcessId']; + /** @description Post a new image for a process. */ + post: operations['postImageByProcessId']; }; '/process/{definitionId}/images/{imageFileName}': { - /** @description Get a specific image og a process. */ - get: operations['getImagesByProcessId']; + /** @description Get a specific image of a process. */ + get: operations['getImageByFilename']; + /** + * @description Update a specific image of a process. + * If imageFileName exists, then it is updated with the body of the request (200 response), if not, then the image is created (201 response). + */ + put: operations['updateImageByFilename']; + /** @description Delete a specific image of a process. */ + delete: operations['deleteImageByFilename']; }; '/process/{definitionId}/user-tasks': { /** @description Get all user tasks used in a process. */ @@ -771,8 +780,8 @@ export interface operations { 403: components['responses']['403_validationFailed']; }; }; - /** @description Get all images used in a process. */ - getImageOfProcessById: { + /** @description Get all image filenames used in a process. */ + getImageFilenamesByProcessId: { parameters: { path: { definitionId: components['parameters']['definitionId']; @@ -782,36 +791,53 @@ export interface operations { /** @description OK */ 200: { content: { - 'image/png image/svg+xml image/jpeg': string; + 'application/json': string[]; }; }; 401: components['responses']['401_unauthenticated']; 403: components['responses']['403_validationFailed']; }; }; - /** @description Get a specific image og a process. */ - getImagesByProcessId: { + /** @description Post a new image for a process. */ + postImageByProcessId: { parameters: { path: { definitionId: components['parameters']['definitionId']; - /** @description Filename of the image */ - imageFileName: string; }; }; - requestBody?: { + /** @description Image file to be stored */ + requestBody: { content: { - 'application/json': { - [key: string]: components['schemas']['image']; + 'image/png image/svg+xml image/jpeg': unknown; + }; + }; + responses: { + /** @description Image created in the server */ + 201: { + content: { + 'application/json': { + imageFileName?: string; + }; }; }; + 401: components['responses']['401_unauthenticated']; + 403: components['responses']['403_validationFailed']; + }; + }; + /** @description Get a specific image of a process. */ + getImageByFilename: { + parameters: { + path: { + definitionId: components['parameters']['definitionId']; + /** @description Filename of the image */ + imageFileName: string; + }; }; responses: { /** @description OK */ 200: { content: { - 'application/json': { - ''?: string; - }; + 'image/png image/svg+xml image/jpeg': string; }; }; 401: components['responses']['401_unauthenticated']; @@ -822,6 +848,55 @@ export interface operations { }; }; }; + /** + * @description Update a specific image of a process. + * If imageFileName exists, then it is updated with the body of the request (200 response), if not, then the image is created (201 response). + */ + updateImageByFilename: { + parameters: { + path: { + definitionId: components['parameters']['definitionId']; + /** @description Filename of the image */ + imageFileName: string; + }; + }; + /** @description Image file to be stored */ + requestBody: { + content: { + 'image/png image/svg+xml image/jpeg': unknown; + }; + }; + responses: { + /** @description Image updated */ + 200: { + content: never; + }; + /** @description Image created in the server */ + 201: { + content: never; + }; + 401: components['responses']['401_unauthenticated']; + 403: components['responses']['403_validationFailed']; + }; + }; + /** @description Delete a specific image of a process. */ + deleteImageByFilename: { + parameters: { + path: { + definitionId: components['parameters']['definitionId']; + /** @description Filename of the image */ + ImageFileName: string; + }; + }; + responses: { + /** @description The request returns 200 wether the image exists or not */ + 200: { + content: never; + }; + 401: components['responses']['401_unauthenticated']; + 403: components['responses']['403_validationFailed']; + }; + }; /** @description Get all user tasks used in a process. */ getUserTasksByProcessId: { parameters: { diff --git a/src/management-system/src/backend/openapi.json b/src/management-system/src/backend/openapi.json index c5337ccf4..59a9bf47f 100644 --- a/src/management-system/src/backend/openapi.json +++ b/src/management-system/src/backend/openapi.json @@ -473,7 +473,7 @@ }, "/process/{definitionId}/images": { "get": { - "operationId": "getImageOfProcessById", + "operationId": "getImageFilenamesByProcessId", "tags": ["Process"], "parameters": [ { @@ -484,10 +484,13 @@ "200": { "description": "OK", "content": { - "image/png image/svg+xml image/jpeg": { + "application/json": { "schema": { - "type": "string", - "description": "Requested image\n" + "type": "array", + "minItems": 0, + "items": { + "type": "string" + } } } } @@ -499,13 +502,53 @@ "$ref": "#/components/responses/403_validationFailed" } }, - "description": "Get all images used in a process." + "description": "Get all image filenames used in a process." + }, + "post": { + "operationId": "postImageByProcessId", + "tags": ["Process"], + "parameters": [ + { + "$ref": "#/components/parameters/definitionId" + } + ], + "requestBody": { + "required": true, + "content": { + "image/png image/svg+xml image/jpeg": {} + }, + "description": "Image file to be stored" + }, + "responses": { + "201": { + "description": "Image created in the server", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "imageFileName": { + "type": "string" + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/401_unauthenticated" + }, + "403": { + "$ref": "#/components/responses/403_validationFailed" + } + }, + "description": "Post a new image for a process." } }, "/process/{definitionId}/images/{imageFileName}": { "get": { - "description": "Get a specific image og a process.", - "operationId": "getImagesByProcessId", + "description": "Get a specific image of a process.", + "operationId": "getImageByFilename", "tags": ["Process"], "parameters": [ { @@ -521,30 +564,14 @@ "description": "Filename of the image" } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/image" - } - } - } - } - }, "responses": { "200": { "description": "OK", "content": { - "application/json": { + "image/png image/svg+xml image/jpeg": { "schema": { - "type": "object", - "properties": { - "": { - "type": "string" - } - } + "type": "string", + "description": "Requested image\n" } } } @@ -559,6 +586,76 @@ "description": "Not Found" } } + }, + "put": { + "description": "Update a specific image of a process.\nIf imageFileName exists, then it is updated with the body of the request (200 response), if not, then the image is created (201 response).", + "operationId": "updateImageByFilename", + "tags": ["Process"], + "parameters": [ + { + "$ref": "#/components/parameters/definitionId" + }, + { + "schema": { + "type": "string" + }, + "name": "imageFileName", + "in": "path", + "required": true, + "description": "Filename of the image" + } + ], + "requestBody": { + "required": true, + "content": { + "image/png image/svg+xml image/jpeg": {} + }, + "description": "Image file to be stored" + }, + "responses": { + "200": { + "description": "Image updated" + }, + "201": { + "description": "Image created in the server" + }, + "401": { + "$ref": "#/components/responses/401_unauthenticated" + }, + "403": { + "$ref": "#/components/responses/403_validationFailed" + } + } + }, + "delete": { + "operationId": "deleteImageByFilename", + "tags": ["Process"], + "parameters": [ + { + "$ref": "#/components/parameters/definitionId" + }, + { + "schema": { + "type": "string" + }, + "name": "ImageFileName", + "in": "path", + "required": true, + "description": "Filename of the image" + } + ], + "responses": { + "200": { + "description": "The request returns 200 wether the image exists or not" + }, + "401": { + "$ref": "#/components/responses/401_unauthenticated" + }, + "403": { + "$ref": "#/components/responses/403_validationFailed" + } + }, + "description": "Delete a specific image of a process." } }, "/process/{definitionId}/user-tasks": { @@ -1412,10 +1509,18 @@ { "type": "object", "properties": { - "username": { "type": "string" }, - "firstName": { "type": "string" }, - "lastName": { "type": "string" }, - "email": { "type": "string" } + "username": { + "type": "string" + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "email": { + "type": "string" + } }, "required": ["username", "firstName", "lastName", "email"] } diff --git a/src/management-system/src/backend/server/index.js b/src/management-system/src/backend/server/index.js index 48903cbe8..9f5eacd48 100644 --- a/src/management-system/src/backend/server/index.js +++ b/src/management-system/src/backend/server/index.js @@ -92,7 +92,10 @@ async function init() { } backendServer.use(express.text({ type: ['text/plain', 'text/html'] })); - backendServer.use(express.json()); + backendServer.use(express.json({ limit: '1mb' })); + backendServer.use( + express.raw({ type: ['image/jpeg', 'image/png', 'image/svg+xml'], limit: '5mb' }), + ); if (config.useAuthorization) { if (config.useAuth0) { diff --git a/src/management-system/src/backend/server/rest-api/process.js b/src/management-system/src/backend/server/rest-api/process.js index 36b4ced49..3a8503717 100644 --- a/src/management-system/src/backend/server/rest-api/process.js +++ b/src/management-system/src/backend/server/rest-api/process.js @@ -11,14 +11,17 @@ import { deleteProcessUserTask, addProcessVersion, getProcessVersionBpmn, - getProcessImages, getProcessImage, + getProcessImageFileNames, + saveProcessImage, + deleteProcessImage, } from '../../shared-electron-server/data/process.js'; import express from 'express'; import { isAllowed } from '../iam/middleware/authorization.ts'; import logger from '../../shared-electron-server/logging.js'; import Ability from '../../../../../management-system-v2/lib/ability/abilityHelper'; import { toCaslResource } from '../../../../../management-system-v2/lib/ability/caslAbility'; +import { v4 } from 'uuid'; const processRouter = express.Router(); @@ -295,7 +298,36 @@ processRouter.get('/:definitionId/images', isAllowed('view', 'Process'), async ( if (!userAbility.can('view', toCaslResource('Process', req.process))) return res.status(403).send('Forbidden.'); - res.status(200).json(await getProcessImages(req.definitionsId)); + res.status(200).json(await getProcessImageFileNames(req.definitionsId)); +}); + +processRouter.post('/:definitionId/images', isAllowed('view', 'Process'), async (req, res) => { + const { definitionId } = req.params; + + const { body: image } = req; + + /** @type {Ability} */ + const userAbility = req.userAbility; + + if (!userAbility.can('update', toCaslResource('Process', req.process))) + return res.status(403).send('Forbidden.'); + + const contentType = req.get('Content-Type'); + const imageType = contentType.split('image/').pop(); + const imageFileName = `_image${v4()}.${imageType}`; + + await saveProcessImage(definitionId, imageFileName, image); + + res.status(201).send({ imageFileName }); +}); + +processRouter.use('/:definitionId/images/:imageFileName', async (req, res, next) => { + req.imageFileName = req.params.imageFileName; + try { + // see if there already exists some data for this user task and make it accessible for later steps + req.image = await getProcessImage(req.definitionsId, req.imageFileName); + } catch (err) {} + next(); }); processRouter.get( @@ -310,9 +342,54 @@ processRouter.get( if (!userAbility.can('view', toCaslResource('Process', req.process))) return res.status(403).send('Forbidden.'); - const image = await getProcessImage(definitionId, imageFileName); res.set({ 'Content-Type': 'image/png image/svg+xml image/jpeg' }); - res.status(200).send(image); + res.status(200).send(req.image); + }, +); + +processRouter.put( + '/:definitionId/images/:imageFileName', + isAllowed('update', 'Process'), + async (req, res) => { + const { definitionId, imageFileName } = req.params; + + const { body: image } = req; + + /** @type {Ability} */ + const userAbility = req.userAbility; + + if (!userAbility.can('update', toCaslResource('Process', req.process))) + return res.status(403).send('Forbidden.'); + + await saveProcessImage(definitionId, imageFileName, image); + + if (!req.image) { + res.status(201); + } else { + res.status(200); + } + + res.end(); + }, +); + +processRouter.delete( + '/:definitionId/images/:imageFileName', + isAllowed('update', 'Process'), + async (req, res) => { + const { definitionId, imageFileName } = req.params; + + /** @type {Ability} */ + const userAbility = req.userAbility; + + if (!userAbility.can('delete', toCaslResource('Process', req.process))) + return res.status(403).send('Forbidden.'); + + if (req.image) { + await deleteProcessImage(definitionId, imageFileName); + } + + res.status(200).send(); }, ); diff --git a/src/management-system/src/backend/shared-electron-server/data/fileHandling.js b/src/management-system/src/backend/shared-electron-server/data/fileHandling.js index 12fccfc74..825b8ce39 100644 --- a/src/management-system/src/backend/shared-electron-server/data/fileHandling.js +++ b/src/management-system/src/backend/shared-electron-server/data/fileHandling.js @@ -270,6 +270,33 @@ export async function getImage(processDefinitionsId, imageFileName) { return image; } +/** + * Returns all image filenames in a process + * + * @param {String} processDefinitionsId + * + * @returns {Promise} + * @resolves {Array} Array containing all fileNames for images + */ +export function getImageFileNames(processDefinitionsId) { + return new Promise((resolve, reject) => { + const imagesDir = getImagesDir(processDefinitionsId); + + if (!fse.existsSync(imagesDir)) { + resolve([]); + } + + fse.readdir(imagesDir, (err, files) => { + if (err) { + reject(err); + return; + } + + resolve(files); + }); + }); +} + /** * Returns all images in a process * @@ -311,9 +338,9 @@ export function getImages(processDefinitionsId) { /** * Saves an image used in a process * - * @param {String} processDefinitionsId the id of the process that contains the user task - * @param {String} imageId the id of the specific user task - * @param {String} image an image of the user task + * @param {String} processDefinitionsId the id of the process that contains the image + * @param {String} imageId the id of the specific image + * @param {String} image an image */ export async function saveImage(processDefinitionsId, imageFileName, image) { const imagesDir = getImagesDir(processDefinitionsId); @@ -325,6 +352,18 @@ export async function saveImage(processDefinitionsId, imageFileName, image) { eventHandler.dispatch('image_changed', { processDefinitionsId, imageFileName, image }); } +/** + * Deletes an image used in a process + * + * @param {String} processDefinitionsId the id of the process that contains the image + * @param {String} imageId the id of the specific image + */ +export async function deleteImage(processDefinitionsId, imageFileName) { + const imagesDir = getImagesDir(processDefinitionsId); + const filePath = path.join(imagesDir, `${imageFileName}`); + fse.unlinkSync(filePath); +} + /** * Returns the html for a user task with the given id in a process * diff --git a/src/management-system/src/backend/shared-electron-server/data/process.js b/src/management-system/src/backend/shared-electron-server/data/process.js index 6a91d0cb9..547a300bd 100644 --- a/src/management-system/src/backend/shared-electron-server/data/process.js +++ b/src/management-system/src/backend/shared-electron-server/data/process.js @@ -8,6 +8,7 @@ import { getImage, getImages, saveImage, + deleteImage, getUserTaskIds, getUserTaskHTML, getUserTasksHTML, @@ -17,6 +18,7 @@ import { getProcessVersion, updateProcess as overwriteProcess, getUpdatedProcessesJSON, + getImageFileNames, } from './fileHandling.js'; import helperEx from '../../../shared-frontend-backend/helpers/javascriptHelpers.js'; const { mergeIntoObject } = helperEx; @@ -394,11 +396,29 @@ export async function getProcessImage(processDefinitionsId, imageFileName) { } } +/** + * Return Array with fileNames of images for given process + * + * @param {String} processDefinitionsId + * @returns {Promise} - contains all image fileNames in the process + */ +export async function getProcessImageFileNames(processDefinitionsId) { + checkIfProcessExists(processDefinitionsId); + + try { + const imageFilenames = await getImageFileNames(processDefinitionsId); + return imageFilenames; + } catch (err) { + logger.debug(`Error getting image filenames. Reason:\n${err}`); + throw new Error('Failed getting all image filenames in process'); + } +} + /** * Return object mapping from images fileNames to their image * * @param {String} processDefinitionsId - * @returns {Object} - contains all images in the process + * @returns {Promise} - contains all images in the process */ export async function getProcessImages(processDefinitionsId) { checkIfProcessExists(processDefinitionsId); @@ -428,6 +448,17 @@ export async function saveProcessImage(processDefinitionsId, imageFileName, imag } } +export async function deleteProcessImage(processDefinitionsId, imageFileName) { + checkIfProcessExists(processDefinitionsId); + + try { + await deleteImage(processDefinitionsId, imageFileName); + } catch (err) { + logger.debug(`Error deleting image. Reason:\n${err}`); + throw new Error('Failed to delete image'); + } +} + /** * Stores the id of the socket wanting to block the process from being deleted inside the process object *