From 8f5e7ca3eba9da00eca069e3bd86cfc8421a7fcf Mon Sep 17 00:00:00 2001 From: weskubo-cgi Date: Thu, 21 Nov 2024 13:38:20 -0800 Subject: [PATCH 1/4] OFMCC-5947 initial commit. --- backend/src/app.js | 2 ++ backend/src/components/files.js | 23 ++++++++++++ backend/src/routes/files.js | 28 +++++++++++++++ .../messages/RequestConversations.vue | 35 ++++++++++++++++--- frontend/src/services/fileService.js | 15 ++++++++ frontend/src/utils/constants.js | 1 + frontend/src/utils/file.js | 21 ++++++++++- 7 files changed, 120 insertions(+), 5 deletions(-) create mode 100644 backend/src/components/files.js create mode 100644 backend/src/routes/files.js create mode 100644 frontend/src/services/fileService.js diff --git a/backend/src/app.js b/backend/src/app.js index 106aa6fb..74c1d300 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -28,6 +28,7 @@ const authRouter = require('./routes/auth') const userRouter = require('./routes/user') const configRouter = require('./routes/config') const documentsRouter = require('./routes/documents') +const filesRouter = require('./routes/files') const healthCheckRouter = require('./routes/healthCheck') const messageRouter = require('./routes/message') const notificationRouter = require('./routes/notification') @@ -263,6 +264,7 @@ apiRouter.use('/auth', authRouter) apiRouter.use('/config', configRouter) apiRouter.use('/documents', documentsRouter) apiRouter.use('/facilities', facilitiesRouter) +apiRouter.use('/files', filesRouter) apiRouter.use('/funding-agreements', fundingAgreementsRouter) apiRouter.use('/health', healthCheckRouter) apiRouter.use('/irregular', irregularApplicationsRouter) diff --git a/backend/src/components/files.js b/backend/src/components/files.js new file mode 100644 index 00000000..2bd28794 --- /dev/null +++ b/backend/src/components/files.js @@ -0,0 +1,23 @@ +'use strict' +const { getOperation, handleError } = require('./utils') +const HttpStatus = require('http-status-codes') +const log = require('./logger') + +async function getFile(req, res) { + try { + let operation = `msdyn_richtextfiles(${req.params.fileId})/` + if (req.query.image) { + operation += 'msdyn_imageblob/$value?size=full' + } else { + operation += 'msdyn_fileblob' + } + const response = await getOperation(operation) + return res.status(HttpStatus.OK).json(response?.value) + } catch (e) { + handleError(res, e) + } +} + +module.exports = { + getFile, +} diff --git a/backend/src/routes/files.js b/backend/src/routes/files.js new file mode 100644 index 00000000..28c9dc13 --- /dev/null +++ b/backend/src/routes/files.js @@ -0,0 +1,28 @@ +const express = require('express') +const passport = require('passport') +const router = express.Router() +const auth = require('../components/auth') +const isValidBackendToken = auth.isValidBackendToken() +const { getFile } = require('../components/files') +const { param, query, validationResult } = require('express-validator') +const validatePermission = require('../middlewares/validatePermission.js') +const { PERMISSIONS } = require('../util/constants') + +module.exports = router + +/** + * Get the file by id + */ +router.get( + '/:fileId', + passport.authenticate('jwt', { session: false }), + isValidBackendToken, + [param('fileId', 'URL param: [fileId] is required').notEmpty().isUUID()], + query('image').optional().isBoolean(), + validatePermission(PERMISSIONS.MANAGE_NOTIFICATIONS), + (req, res) => { + validationResult(req).throw() + return getFile(req, res) + }, +) +module.exports = router diff --git a/frontend/src/components/messages/RequestConversations.vue b/frontend/src/components/messages/RequestConversations.vue index 4b70af20..1bda30d3 100644 --- a/frontend/src/components/messages/RequestConversations.vue +++ b/frontend/src/components/messages/RequestConversations.vue @@ -87,14 +87,19 @@ diff --git a/frontend/src/services/fileService.js b/frontend/src/services/fileService.js new file mode 100644 index 00000000..ed5f83fe --- /dev/null +++ b/frontend/src/services/fileService.js @@ -0,0 +1,15 @@ +import ApiService from '@/common/apiService' +import { ApiRoutes } from '@/utils/constants' + +export default { + async getFile(fileId, image = false) { + try { + if (!fileId) return null + const response = await ApiService.apiAxios.get(`${ApiRoutes.FILES}/${fileId}?image=${image}`) + return response.data + } catch (error) { + console.log(`Failed to get file - ${error}`) + throw error + } + }, +} diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js index 0a749829..b4c0de95 100644 --- a/frontend/src/utils/constants.js +++ b/frontend/src/utils/constants.js @@ -16,6 +16,7 @@ export const ApiRoutes = Object.freeze({ FACILITIES: baseRoot + '/facilities', FACILITIES_CONTACTS: baseRoot + '/facilities/:facilityId/contacts', FACILITIES_LICENCES: baseRoot + '/facilities/:facilityId/licences', + FILES: baseRoot + '/files', FUNDING_AGREEMENTS: baseRoot + '/funding-agreements', LICENCES: baseRoot + '/licences', LOOKUP: baseRoot + '/config/lookup', diff --git a/frontend/src/utils/file.js b/frontend/src/utils/file.js index ddb5f2ad..5566dfc7 100644 --- a/frontend/src/utils/file.js +++ b/frontend/src/utils/file.js @@ -3,7 +3,6 @@ * @param {*} bytes * @param {*} decimals */ - export function humanFileSize(bytes, decimals = 2) { if (bytes === 0) return '0 Bytes' const k = 1024 @@ -40,3 +39,23 @@ export function updateHeicFileNameToJpg(filename) { const regex = /\.heic(?![\s\S]*\.heic)/i //looks for last occurrence of .heic case-insensitive return filename?.replace(regex, '.jpg') } + +/** + * Quick and dirty way to determine image type based on base64 encoding. + * @param image The base64 encoded image + */ +export function deriveImageType(image) { + const key = image.charAt(0) + switch (key) { + case '/': + return 'jpg' + case 'i': + return 'png' + case 'R': + return 'gif' + case 'U': + return 'webp' + default: + return '*' + } +} From ca8d3eb59b6f6bf3ecc212c4510c0b61f3e37611 Mon Sep 17 00:00:00 2001 From: weskubo-cgi Date: Thu, 21 Nov 2024 16:24:24 -0800 Subject: [PATCH 2/4] Removed unused import. --- backend/src/components/files.js | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/components/files.js b/backend/src/components/files.js index 2bd28794..fdba983b 100644 --- a/backend/src/components/files.js +++ b/backend/src/components/files.js @@ -1,7 +1,6 @@ 'use strict' const { getOperation, handleError } = require('./utils') const HttpStatus = require('http-status-codes') -const log = require('./logger') async function getFile(req, res) { try { From c0d5f0e10512f793f3875e19b49ea24880e34513 Mon Sep 17 00:00:00 2001 From: weskubo-cgi Date: Fri, 22 Nov 2024 11:11:43 -0800 Subject: [PATCH 3/4] Refined code. Removed images that can't be replaced. Removed hyperlinks to CRM attachments. --- .../messages/RequestConversations.vue | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/messages/RequestConversations.vue b/frontend/src/components/messages/RequestConversations.vue index 1bda30d3..8067152b 100644 --- a/frontend/src/components/messages/RequestConversations.vue +++ b/frontend/src/components/messages/RequestConversations.vue @@ -98,7 +98,6 @@ import { ASSISTANCE_REQUEST_STATUS_CODES, OFM_PROGRAM } from '@/utils/constants' import { deriveImageType } from '@/utils/file' import format from '@/utils/format' -const CRM_PATH = '/api/data' const ID_REGEX = /\(([^)]+)\)/ export default { @@ -212,22 +211,24 @@ export default { }, async formatConversation() { const parser = new DOMParser() - // Lookup for occurences of CRM imageblobs and replace with custom API call for (const conversation of this.assistanceRequestConversation) { const document = parser.parseFromString(conversation.message, 'text/html') - for (const img of document.querySelectorAll('img')) { - const src = img.getAttribute('src') - // Validate the path in case there are externally linked images - if (src?.startsWith(CRM_PATH)) { - const matches = ID_REGEX.exec(src) - if (matches) { - const fileId = matches[1] - const image = await FileService.getFile(fileId) - const imageType = deriveImageType(image) - img.setAttribute('src', `data:image/${imageType};base64,${image}`) - } + // Update CRM img tags to load correctly + for (const img of document.querySelectorAll('img[src*="/api/data"]')) { + const matches = ID_REGEX.exec(img.getAttribute('src')) + if (matches) { + const fileId = matches[1] + const image = await FileService.getFile(fileId) + const imageType = deriveImageType(image) + img.setAttribute('src', `data:image/${imageType};base64,${image}`) + } else { + img.remove() } } + // Remove CRM links for now until we can support them + document.querySelectorAll('a[href*="/api/data"]').forEach((link) => link.remove()) + + // Update the message content conversation.message = document.documentElement.innerHTML } }, From dfa5ed4d9909c0e2a9fcc2ca032aeece9d58019c Mon Sep 17 00:00:00 2001 From: weskubo-cgi Date: Fri, 22 Nov 2024 11:29:41 -0800 Subject: [PATCH 4/4] Minor update. --- frontend/src/utils/file.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/utils/file.js b/frontend/src/utils/file.js index 5566dfc7..0eb0060e 100644 --- a/frontend/src/utils/file.js +++ b/frontend/src/utils/file.js @@ -45,8 +45,7 @@ export function updateHeicFileNameToJpg(filename) { * @param image The base64 encoded image */ export function deriveImageType(image) { - const key = image.charAt(0) - switch (key) { + switch (image.charAt(0)) { case '/': return 'jpg' case 'i':