diff --git a/.gitignore b/.gitignore index 68f5a24..4d85769 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /node_modules .env /common/* +.idea/ \ No newline at end of file diff --git a/databindingsHandler.js b/databindingsHandler.js index c2f5d26..1a3a65e 100644 --- a/databindingsHandler.js +++ b/databindingsHandler.js @@ -21,7 +21,7 @@ async function populateDatabindings(formJson, params) { } // Map the API data to the form fields - const boundData = bindDataToFields(formJson, apiData); + const boundData = bindDataToFields(formJson, apiData, params); return boundData; } catch (error) { @@ -40,6 +40,7 @@ async function fetchDataFromSources(dataSources, params) { let updatedParams = updateParams(source.params || {}, params, data); const response = await readJsonFormApi(source, { ...params, ...updatedParams }); + //console.log("Response:",response); data[source.name] = response; } catch (error) { @@ -51,80 +52,106 @@ async function fetchDataFromSources(dataSources, params) { return data; } -function bindDataToFields(formJson, fetchedData) { +function getBindingValue(bindings, fetchedData, params = {}) { + // Normalize singular binding to array if needed + let list; + if (Array.isArray(bindings)) { + list = bindings; + } else if (bindings && bindings.source && bindings.path) { + list = [{ + order: 0, + condition: '', + source: bindings.source, + path: bindings.path + }]; + } else { + return null; + } + + const sortedBindings = list.slice().sort((a, b) => (a.order || 0) - (b.order || 0)); + + // Evaluate each binding + for (const binding of sortedBindings) { + // Parse condition + const [conditionField, valuesListString = ''] = (binding.condition || '').split('=').map(str => str.trim()); + + // Evaluate condition + if (conditionField) { + const allowedValues = valuesListString.split(',').map(str => str.trim()); + const currentParam = params[conditionField]; + if (!allowedValues.includes(currentParam)) { + continue; + } + } + + const sourceData = fetchedData[binding.source] || {}; + const results = JSONPath({ path: binding.path, json: sourceData }); + + if (Array.isArray(results) && results.length > 0 && results[0] != null && results[0] !== '') { + return results[0]; + } + } + + return null; +} + +function bindDataToFields(formJson, fetchedData, params = {}) { const formData = {}; - const processItemsForDatabinding = (items) => { - items.forEach(field => { + //helper function to bind group + function bindRowsToGroup(field, rows) { + return rows.map((rowObj, index) => { + const row = {}; + const binding = Array.isArray(field.databindings) ? field.databindings[0] : field.databindings || null; + const localDatabindings = binding ? { ...fetchedData, [binding.source]: rowObj } : { ...fetchedData }; + + field.groupItems.forEach(groupItem => + groupItem.fields.forEach(groupField => { + if (!groupField.databindings) return; + const fieldIdInGroup = `${field.id}-${index}-${groupField.id}`; + const subVal = getBindingValue(groupField.databindings,localDatabindings,params); + row[fieldIdInGroup] = transformValueToBindIfNeeded(groupField, subVal) || ''; + }) + ); + return row; + }); + } + function processItemsForDatabinding(items) { + items.forEach(field => { + // containers if (field.type === 'container' && field.containerItems) { - processItemsForDatabinding(field.containerItems); - } else if (field.databindings != null) { - - const dataSourceName = field.databindings.source; - const dataPath = field.databindings.path; - const fetchedSourceData = fetchedData[dataSourceName]; - let valueFromPath = ""; - - // Fetch the value from the fetched data - if (fetchedSourceData) { - valueFromPath = JSONPath(dataPath, fetchedSourceData); - // If the field is a group, handle groupItems - if (field.type === 'group' && field.groupItems) { - // Create an array to store groupData objects for each item in valueFromPath - const groupDataArray = valueFromPath.map((pathObj, index) => { - const groupData = {}; - - // For each item in valueFromPath, iterate over the group fields - field.groupItems.forEach(groupItem => { - groupItem.fields.forEach(groupField => { - if (groupField.databindings) { - const fieldBindings = groupField.databindings.path; - const fieldIdInGroup = `${field.id}-${index}-${groupField.id}`; - // Assign data from pathObj or an empty string if not available - groupData[fieldIdInGroup] = transformValueToBindIfNeeded(groupField, pathObj[fieldBindings]) || ''; - } - }); - }); - return groupData; - }); - - // Assign the groupDataArray to formData for this field's ID - formData[field.id] = groupDataArray; - } else { - formData[field.id] = valueFromPath.length > 0 ? transformValueToBindIfNeeded(field, valueFromPath[0]) : null; - } + return processItemsForDatabinding(field.containerItems); + } + + // groups + if (field.type === 'group' && field.groupItems) { + if (field.repeater) { + // repeatable group + const binding = Array.isArray(field.databindings) ? field.databindings[0] : field.databindings || null; + const rows = binding ? JSONPath({ path: binding.path, json: fetchedData }) || [] : []; + formData[field.id] = bindRowsToGroup(field, rows); + } else { + // single-instance group + const binding = Array.isArray(field.databindings) ? field.databindings[0]: field.databindings || null; + const rows = binding? (JSONPath({ path: binding.path, json: fetchedData }) || []).slice(0, 1) : [{}]; + formData[field.id] = bindRowsToGroup(field, rows); } - } else if (field.type === 'group' && field.groupItems && !field.repeater) { - const transformedItem = {}; - //get the databindings from individual filed from non-repeating group - field.groupItems.forEach(groupItem => { - groupItem.fields.forEach(groupField => { - if (groupField.databindings) { - const dataSourceName = groupField.databindings.source; - const dataPath = groupField.databindings.path; - const fetchedSourceData = fetchedData[dataSourceName]; - const fieldIdInGroup = `${field.id}-0-${groupField.id}`; - if (fetchedSourceData) { - const valueFromPathForGroupField = JSONPath(dataPath, fetchedSourceData); - //do date conversion here uisng a function - chceking the type as date and then checking format and applying - transformedItem[fieldIdInGroup] = valueFromPathForGroupField.length > 0 ? transformValueToBindIfNeeded(groupField, valueFromPathForGroupField[0]) : null; // Replace with actual value - } - } - }); - }); - formData[field.id] = [transformedItem]; + return; + } + + //Single fields + if (field.databindings) { + const raw = getBindingValue(field.databindings, fetchedData, params); + formData[field.id] = raw != null ? transformValueToBindIfNeeded(field, raw) : null; } }); - } + }; - if (formJson?.data?.items) { + if (Array.isArray(formJson?.data?.items)) { processItemsForDatabinding(formJson.data.items); } - - // Iterate through fields - formJson.data.items.forEach - + // console.dir(formData, { depth: null, colors: true }); return formData; } @@ -161,6 +188,7 @@ async function readJsonFormApi(datasource, pathParams) { const headers = { Authorization: `Bearer ${grant.id_token.token}`, "X-ICM-TrustedUsername": username, + "X-API-Host": apiHost, } if (type.toUpperCase() === 'GET') { // For GET requests, add params directly in axios config @@ -173,6 +201,7 @@ async function readJsonFormApi(datasource, pathParams) { } // Store response data + //console.log("Response:",response); return ensureObjectOrArray(response.data); } catch (error) { @@ -184,6 +213,10 @@ async function readJsonFormApi(datasource, pathParams) { function buildUrlWithParams(host, endpoint, pathVariables) { const hostFromEnv = getHost(pathVariables,host); const endpointFromEnv = getEndpoint(endpoint); + + if (!hostFromEnv) throw new Error("API host not resolved"); + if (!endpointFromEnv) throw new Error("API endpoint not resolved"); + let url = `${hostFromEnv}${endpointFromEnv}`; // Replace any placeholder variables like @@attachmentId Object.keys(pathVariables).forEach(key => { @@ -196,18 +229,23 @@ function buildUrlWithParams(host, endpoint, pathVariables) { function getHost(params, host) { - // Use host from environment variable if available, otherwise fall back to JSON - try { - return params["apiHost"]; - } catch { - return process.env[host]; + // Use process.env if present + const fromEnv = resolveMaybeEnv(host); + if (fromEnv) return fromEnv; + + //Fall back to path params + if (params && typeof params.apiHost === "string" && params.apiHost.trim()) { + return params.apiHost.trim(); } + + return host ? (process.env[host] ?? host) : null; } function getEndpoint(endpoint) { // Use endpoint from environment variable if available, otherwise fall back to JSON - return process.env[endpoint] || endpoint; + return resolveMaybeEnv(endpoint) ?? endpoint; } + function buildBodyWithParams(bodyFromJson, pathVariables) { let bodyString = JSON.stringify(bodyFromJson); @@ -290,5 +328,25 @@ function updateParams(params, pathParams = {}, allFetchedData = {}) { return updated; } +function resolveMaybeEnv(str) { + if (typeof str !== "string" || !str) { + return null; + } + + // Check if value is in the form "process.env.VAR_NAME" + const envPattern = /^process\.env\.(.+)$/; + const match = str.match(envPattern); + + if (!match) { + return str; + } + + const envVarName = match[1]; + const envValue = process.env[envVarName]; + + return envValue != null ? envValue : str; +} + + module.exports.populateDatabindings = populateDatabindings; module.exports.buildUrlWithParams = buildUrlWithParams; diff --git a/errorHandling/errorMessages.json b/errorHandling/errorMessages.json index 12207ff..30b42f2 100644 --- a/errorHandling/errorMessages.json +++ b/errorHandling/errorMessages.json @@ -12,7 +12,15 @@ "FORM_NOT_FOUND_IN_REQUEST":"The form is not found in the incoming request. Try again later and report to support, if the issue is not solved", "GENERATE_ERROR_MSG":"Error generating the form .Close this window and try again in a few seconds", "FORM_ALREADY_FINALIZED":"The form you are trying to generate is already finalized", - "FORM_CANNOT_BE_GENERATED":"The form cannot be generated." + "FORM_CANNOT_BE_GENERATED":"The form cannot be generated.", + "INVALID_FORM_ID":"Invalid Form ID.", + "INVALID_TOOL":"Invalid Tool.", + "INVALID_AREA":"Invalid Area.", + "PARAMS_NOT_FOUND":"Parameters not found to generate form", + "UNKNOWN_ORIGIN_SERVER":"Unknown Origin Server", + "NO_ACTION_FOUND":"No action found", + "UNKNOWN_ACTION":"Unknown action encountered", + "ERROR_IN_EXECUTING_ACTION":"Error in executing action type" }, "test": { "FORM_ID_REQUIRED": "Close this window and try again in a few seconds", @@ -27,7 +35,15 @@ "FORM_NOT_FOUND_IN_REQUEST":"Try again later and report to support, if the issue is not solved", "GENERATE_ERROR_MSG":"Close this window and try again in a few seconds", "FORM_ALREADY_FINALIZED":"The form you are trying to generate is already finalized", - "FORM_CANNOT_BE_GENERATED":"The form cannot be generated." + "FORM_CANNOT_BE_GENERATED":"The form cannot be generated.", + "INVALID_FORM_ID":"Invalid Form ID.", + "INVALID_TOOL":"Invalid Tool.", + "INVALID_AREA":"Invalid Area.", + "PARAMS_NOT_FOUND":"Parameters not found to generate form", + "UNKNOWN_ORIGIN_SERVER":"Unknown Origin Server", + "NO_ACTION_FOUND":"No action found", + "UNKNOWN_ACTION":"Unknown action encountered", + "ERROR_IN_EXECUTING_ACTION":"Error in executing action type" }, "prod": { "FORM_ID_REQUIRED": "Close this window and try again in a few seconds", @@ -42,6 +58,14 @@ "FORM_NOT_FOUND_IN_REQUEST":"Try again later and report to support, if the issue is not solved", "GENERATE_ERROR_MSG":"Close this window and try again in a few seconds", "FORM_ALREADY_FINALIZED":"The form you are trying to generate is already finalized", - "FORM_CANNOT_BE_GENERATED":"The form cannot be generated." + "FORM_CANNOT_BE_GENERATED":"The form cannot be generated.", + "INVALID_FORM_ID":"Invalid Form ID.", + "INVALID_TOOL":"Invalid Tool.", + "INVALID_AREA":"Invalid Area.", + "PARAMS_NOT_FOUND":"Parameters not found to generate form", + "UNKNOWN_ORIGIN_SERVER":"Unknown Origin Server", + "NO_ACTION_FOUND":"No action found", + "UNKNOWN_ACTION":"Unknown action encountered", + "ERROR_IN_EXECUTING_ACTION":"Error in executing action type" } } \ No newline at end of file diff --git a/generateHandler.js b/generateHandler.js index 9d4fd3c..f764690 100644 --- a/generateHandler.js +++ b/generateHandler.js @@ -141,6 +141,34 @@ async function generateNewTemplate(req, res) { .send({ error: getErrorMessage("FORM_ALREADY_FINALIZED") }); } + if (icm_metadata["Template"] !== template_id) { + return res + .status(400) + .send({ error: getErrorMessage("INVALID_FORM_ID") }); + } + + if (icm_metadata["Tool"]?.toLowerCase() !== "formfoundry") { + return res + .status(400) + .send({ error: getErrorMessage("INVALID_TOOL") }); + } + + const requestArea = params.area?.toLowerCase(); + + if (requestArea) { + const icmArea = icm_metadata.Categorie?.toLowerCase(); + + const isServiceRequest = requestArea === "service request"; + + const isAreaValid = + (isServiceRequest && icmArea === "application") || + (!isServiceRequest && requestArea === icmArea); + + if (!isAreaValid) { + return res.status(400).send({ error: getErrorMessage("INVALID_AREA") }); + } + } + const formJson = await constructFormJson(template_id, params); if (formJson != null) { diff --git a/helm/values.yaml b/helm/values.yaml index 5acaabf..56ee505 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -11,7 +11,13 @@ service: configMapName: commlayer -resources: {} +resources: + requests: + memory: "128Mi" + cpu: "50m" + limits: + memory: "512Mi" + cpu: "250m" nodeSelector: {} diff --git a/icmJsonClobHandler.js b/icmJsonClobHandler.js new file mode 100644 index 0000000..a17dcd3 --- /dev/null +++ b/icmJsonClobHandler.js @@ -0,0 +1,99 @@ +const axios = require("axios"); +const { keycloakForSiebel } = require("./keycloak.js"); + +function parseIcmJsonClob(item) { + if (item.ICMJsonClob && typeof item.ICMJsonClob === 'string') { + try { + item.ICMJsonClobParsed = JSON.parse(item.ICMJsonClob); + delete item.ICMJsonClob; + } catch (error) { + console.warn(`Failed to parse ICMJsonClob for item ${item.Id || 'unknown'}: ${error.message}`); + item.ICMJsonClobParsed = null; + } + } + return item; +} + +function processIcmJsonClobData(jsonData) { + + // Create a copy to avoid mutating the original instance + const processedData = JSON.parse(JSON.stringify(jsonData)); + + if (processedData.items && Array.isArray(processedData.items)) { + processedData.items = processedData.items.map(parseIcmJsonClob); + return processedData; + } + + return parseIcmJsonClob(processedData); +} + +async function fetchIcmJsonClobData(attachmentId, apiHost) { + try { + const grant = await keycloakForSiebel.grantManager.obtainFromClientCredentials(); + + const username = process.env.TRUSTED_USERNAME; + + const headers = { + Authorization: `Bearer ${grant.id_token.token}`, + "X-ICM-TrustedUsername": username, + "Content-Type": "application/json" + }; + + const url = `${apiHost}${process.env.ICM_JSON_CLOB_ENDPONT}`; + + const queryParams = { + ViewMode: "Catalog", + SearchSpec: `UserFieldCLOB=\"${attachmentId}\"` + }; + + return await axios.get(url, { + headers, + params: queryParams, + timeout: 30000 + }); + + } catch (error) { + throw new Error(`Failed to fetch ICM data for SR ID ${attachmentId}: ${error.message}`); + } +} + +function extractICMJsonClobParsed(data) { + if (data.items && Array.isArray(data.items)) { + return data.items + .map(item => item.ICMJsonClobParsed) + .filter(parsed => parsed !== null && parsed !== undefined); + } + + if (data.ICMJsonClobParsed !== null && data.ICMJsonClobParsed !== undefined) { + return data.ICMJsonClobParsed; + } + + return []; +} + +async function getProcessedData(attachmentId, apiHost, returnAnswersOnly = true) { + try { + const response = await fetchIcmJsonClobData(attachmentId, apiHost); + let processedResult = processIcmJsonClobData(response.data); + + if (returnAnswersOnly) { + processedResult = extractICMJsonClobParsed(processedResult); + } + + return { + success: true, + data: processedResult + }; + } catch (error) { + console.error("API call failed:", error.message); + + return { + success: false, + error: error.message + }; + } +} + +module.exports = { + getProcessedData +}; \ No newline at end of file diff --git a/portal/generatePortalIntegratedHandler.js b/portal/generatePortalIntegratedHandler.js index 87dcd8e..582b08d 100644 --- a/portal/generatePortalIntegratedHandler.js +++ b/portal/generatePortalIntegratedHandler.js @@ -8,34 +8,29 @@ const {getParametersFromPortal , expireTokenInPortal} = require("./loadPortalDa async function generatePortalIntegratedTemplate(req, res) { try { const params = req.body; - const token = params["id"]; + const token = params["id"]; + const userId = "test"; if (!token ) { return res .status(400) .send({ error: 'No token found' }); - } - const userId = "test"; - const portalId = params["portalId"]; - //const targetApp = appConfig[portalId]; + } const rawHost = (req.get("X-Original-Server") || req.hostname); const targetApp = appConfig[rawHost]; if(!targetApp) { - return res.status(400).send({ error: 'Unknown app ID' }); + return res.status(400).send({ error: getErrorMessage("UNKNOWN_ORIGIN_SERVER") }); } - const paramsFromPortal = await getParametersFromPortal(targetApp,token,userId); + const paramsFromPortal = await getParametersFromPortal(targetApp,token,userId); if(!paramsFromPortal) { - return res.status(400).send({ error: 'Parameters not found to generate form' }); + return res.status(400).send({ error: getErrorMessage("PARAMS_NOT_FOUND") }); } - const formJson = await constructFormJson(paramsFromPortal); if (formJson != null) { - //expire token - const isTokenExpired = await expireTokenInPortal(targetApp,token,userId); - console.log("isTokenExpired >> ",isTokenExpired); + res.status(200).send({ save_data: formJson }); @@ -46,9 +41,10 @@ async function generatePortalIntegratedTemplate(req, res) { } } catch (error) { console.error(`Error generating the form:`, error); + return res .status(400) - .send({ error: getErrorMessage("GENERATE_ERROR_MSG") }); + .send({ error: getErrorMessage("FORM_CANNOT_BE_GENERATED") }); } } @@ -71,4 +67,6 @@ async function constructFormJson(params) { return fullJSON; } + + module.exports = generatePortalIntegratedTemplate; \ No newline at end of file diff --git a/portal/loadPortalDataHandler.js b/portal/loadPortalDataHandler.js index 799e83f..0c8eabf 100644 --- a/portal/loadPortalDataHandler.js +++ b/portal/loadPortalDataHandler.js @@ -1,13 +1,11 @@ const axios = require("axios"); const { getErrorMessage } = require("../errorHandling/errorHandler.js"); -async function getParametersFromPortal(portal,token, userId) { - //call another api from portal to get the params - //TODO - add header for validation +async function getParametersFromPortal(portal,token, userId) { let parametersForForm = ""; try { - const urlForValidateTokenAndGetParams= portal.baseUrl+ (portal.getParametersEndpoint || process.env.PORTAL_VALIDATE_TOKEN_ENDPOINT); - console.log("urlForValidateTokenAndGetParams",urlForValidateTokenAndGetParams); + const urlForValidateTokenAndGetParams= portal.apiHost+ (portal.getParametersEndpoint || process.env.PORTAL_VALIDATE_TOKEN_ENDPOINT); + console.log("urlForValidateTokenAndGetParams >>",urlForValidateTokenAndGetParams); const response = await axios.post(`${urlForValidateTokenAndGetParams}`, { token, @@ -18,14 +16,16 @@ async function getParametersFromPortal(portal,token, userId) { 'Content-Type': 'application/json' } } - ); - - parametersForForm = response.data; + ); + return response.data; + + } catch (err) { - console.log( 'Failed to contact target app', err ); - return parametersForForm; + console.log( 'Failed to contact target app', err ); + + throw err; } - return parametersForForm; + } async function expireTokenInPortal(portal,token, userId) { @@ -34,7 +34,7 @@ async function expireTokenInPortal(portal,token, userId) { let isTokenExpired = false; try { - const urlForExpiringToken = portal.baseUrl + (portal.expireTokenEndPoint || process.env.PORTAL_EXPIRE_TOKEN_ENDPOINT);; + const urlForExpiringToken = portal.apiHost + (portal.expireTokenEndPoint || process.env.PORTAL_EXPIRE_TOKEN_ENDPOINT);; console.log("urlForExpiringToken",urlForExpiringToken); const response = await axios.post(`${urlForExpiringToken}`, { @@ -55,4 +55,33 @@ async function expireTokenInPortal(portal,token, userId) { return isTokenExpired; } -module.exports = {getParametersFromPortal , expireTokenInPortal}; \ No newline at end of file +async function getSavedFormFromPortal(portal,token, userId) { + let parametersForForm = ""; + + try { + const urlForValidateTokenAndGetJson= portal.apiHost+ (portal.getSavedJsonEndpoint || process.env.PORTAL_VALIDATE_TOKEN_ENDPOINT); + console.log("urlForValidateTokenAndGetJson >>",urlForValidateTokenAndGetJson); + const response = await axios.post(`${urlForValidateTokenAndGetJson}`, + { + token, + userId + }, + { + headers: { + 'Content-Type': 'application/json' + } + } + ); + //TODO: verify the json against schema or some other sanity checks + return response.data; + + + } catch (err) { + console.log( 'Failed to contact target app', err ); + + throw err; + } + +} + +module.exports = {getParametersFromPortal , expireTokenInPortal, getSavedFormFromPortal }; \ No newline at end of file diff --git a/portal/loadPortalIntegratedHandler.js b/portal/loadPortalIntegratedHandler.js new file mode 100644 index 0000000..0b7c2b1 --- /dev/null +++ b/portal/loadPortalIntegratedHandler.js @@ -0,0 +1,48 @@ +const { keycloakForFormRepo } = require("../keycloak.js"); +const axios = require("axios"); +const getFormFromFormTemplate = require("../formRepoHandler.js"); +const { getErrorMessage } = require("../errorHandling/errorHandler.js"); +const appConfig = require('../appConfig.js'); +const {getSavedFormFromPortal} = require("./loadPortalDataHandler.js"); + +async function loadPortalIntegratedForm(req, res) { + try { + const params = req.body; + const token = params["id"]; + const userId = "test"; + if (!token ) { + return res + .status(400) + .send({ error: 'No token found' }); + } + + const rawHost = (req.get("X-Original-Server") || req.hostname); + const targetApp = appConfig[rawHost]; + + if(!targetApp) { + return res.status(400).send({ error: getErrorMessage("UNKNOWN_ORIGIN_SERVER") }); + } + const formJson = await getSavedFormFromPortal(targetApp,token,userId); + + if(!formJson) { + return res.status(400).send({ error: getErrorMessage("FORM_NOT_FOUND", { templateId: template_id }) }); + } + //the formJson is a base64 string . Converting to json here. + const savedJson = Buffer.from(formJson["form"], 'base64').toString('utf-8'); + const data = JSON.parse(savedJson); + res.status(200).send(data); + + } catch (error) { + console.error(`Error generating the form:`, error); + + return res + .status(400) + .send({ error: getErrorMessage("FORM_CANNOT_BE_GENERATED") }); + } +} + + + + + +module.exports = loadPortalIntegratedForm; \ No newline at end of file diff --git a/portal/savePortalFormDataHandler.js b/portal/savePortalFormDataHandler.js new file mode 100644 index 0000000..0bc06d4 --- /dev/null +++ b/portal/savePortalFormDataHandler.js @@ -0,0 +1,68 @@ +const appConfig = require('../appConfig.js'); +async function submitForPortalAction (req,res) { + const { tokenId, savedForm ,config} = req.body; + try{ + + if (!config?.actions || !Array.isArray(config.actions)) { + return res + .status(400) + .send({ error: getErrorMessage("NO_ACTION_FOUND")}); + } + const rawHost = (req.get("X-Original-Server") || req.hostname); + const portalConfig = appConfig[rawHost] || Object.values(appConfig).find(cfg => { + try { + return new URL(cfg.apiHost).hostname === rawHost; + } catch { + return false; + } + }) || {}; + + for (const action of config.actions) { + if (action.action_type === 'endpoint') { + await handleEndpointAction(tokenId,action, savedForm,portalConfig); + } else { + console.warn('Unexpected action type:', action.action_type); + return res + .status(400) + .send({ error: getErrorMessage("UNKNOWN_ACTION") }); + } + } + + res.json({ status: 'success' }); + } catch(error) { + return res + .status(400) + .send({ error: getErrorMessage("ERROR_IN_EXECUTING_ACTION") }); + } +} +async function handleEndpointAction(tokenId,action, formData, portalConfig) { + + try { + const portalHost = portalConfig["apiHost"] || action.host; + const url = portalHost+action.path; + const headers = Object.fromEntries(action.headers.map(h => Object.entries(h)[0])); + + const savedJson = { + "token": tokenId, + "formJson": formData + }; + const actionBody = Object.fromEntries( + (action.body || []).map(b => Object.entries(b)[0]) + ); + const payload = { ...savedJson, ...actionBody }; + const response = await fetch(url, { + method: action.type, + headers: { ...headers, Authorization: `Bearer ${action.authentication}`, 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + if (!response.ok) { + const text = await response.text(); + throw new Error(`Endpoint error: ${response.status} ${text}`); + } + } catch(error) { + console.error('Error in handleEndPoint:', error); + throw error; + } +} +module.exports = submitForPortalAction; \ No newline at end of file diff --git a/routes.js b/routes.js index a98c753..3a1a28a 100644 --- a/routes.js +++ b/routes.js @@ -6,17 +6,39 @@ const { generateTemplate,generateNewTemplate } = require("./generateHandler"); const { saveICMdata, loadICMdata, clearICMLockedFlag } = require("./saveICMdataHandler"); const { getUsername } = require("./usernameHandler.js"); const renderRouter = require("./renderHandler"); +const appCfg = require('./appConfig.js'); const {generatePDFFromHTML,generatePDFFromURL,generatePDFFromJSON,loadSavedJson } = require("./generatePDFHandler"); const generatePortalIntegratedTemplate = require("./portal/generatePortalIntegratedHandler.js"); - - -const getFormsFromFormTemplate = require("./formRepoHandler"); +const submitForPortalAction = require("./portal/savePortalFormDataHandler.js"); +const loadPortalIntegratedForm = require("./portal/loadPortalIntegratedHandler.js"); +require("./formRepoHandler"); +const {getProcessedData} = require("./icmJsonClobHandler"); const router = express.Router(); const FORM_SERVER_URL = process.env.FORMSERVERURL; const ENDPOINT_URL = process.env.ENDPOINTURL; +const localhostOnlyMiddleware = (req, res, next) => { + const clientIP = req.ip || + req.connection.remoteAddress || + req.socket.remoteAddress || + (req.connection.socket ? req.connection.socket.remoteAddress : null); + + const actualIP = clientIP?.replace('::ffff:', '') || clientIP; + + const isLocalhost = clientIP === '127.0.0.1' || + clientIP === '::1' || + clientIP === 'localhost' || + actualIP?.startsWith('172.') || // Docker default bridge network + actualIP?.startsWith('192.168.') || // Docker custom networks + actualIP?.startsWith('10.'); // Docker swarm networks + + if (!isLocalhost) { + return res.status(403).json({ error: 'Localhost access only' }); + } + next(); +}; // Form Map const formMap = new Map(); @@ -98,7 +120,7 @@ router.get("/xml", async (req, res) => { // Save data route router.post("/saveData", async (request, response) => { - console.log(request.body); + //console.log(request.body); response.status(200).json("{success!}"); }); @@ -113,8 +135,7 @@ router.post("/generate", generateTemplate); router.get("/getAllForms", async (request, response) => { try { - const grant = - await keycloakForFormRepo.grantManager.obtainFromClientCredentials(); + const grant = await keycloakForFormRepo.grantManager.obtainFromClientCredentials(); let endpointUrl = `http://localhost:3030/api/forms-list`; const forms = await axios.get(endpointUrl, { @@ -122,7 +143,7 @@ router.get("/getAllForms", async (request, response) => { Authorization: `Bearer ${grant.access_token.token}`, }, }); - console.log("forms", forms); + //console.log("forms", forms); response.json(forms.data); } catch (err) { @@ -132,10 +153,41 @@ router.get("/getAllForms", async (request, response) => { }); +router.get("/processIcmJsonClob", localhostOnlyMiddleware, async (req, res) => { + try { + const apiHost = req.get("X-API-Host"); + const result = await getProcessedData(req.query.attachmentId, apiHost, false); + + if (result.success) { + res.status(200).json(result.data); + } else { + res.status(500).json({ error: "Error." }); + } + } catch (error) { + console.error("Error:", error.message); + res.status(500).json({ error: "Internal server error", message: error.message }); + } +}); + +router.get("/processIcmJsonClobAnswers", localhostOnlyMiddleware, async (req, res) => { + try { + const apiHost = req.get("X-API-Host"); + const result = await getProcessedData(req.query.attachmentId, apiHost, true); + + if (result.success) { + res.status(200).json(result.data); + } else { + res.status(500).json({ error: "Error." }); + } + } catch (error) { + console.error("Error:", error.message); + res.status(500).json({ error: "Internal server error", message: error.message }); + } +}); + // clear the locked by flags in ICM for the form, used when form is closed router.post("/clearICMLockedFlag", clearICMLockedFlag); - router.post("/generatePDFFromJson", generatePDFFromJSON); // Generate route @@ -145,5 +197,7 @@ router.post("/loadSavedJson", loadSavedJson); router.post("/generatePortalForm", generatePortalIntegratedTemplate); router.post("/generateNewTemplate", generateNewTemplate); router.use('/pdfRender', renderRouter); +router.use('/submitForPortalAction', submitForPortalAction); +router.post("/loadPortalForm", loadPortalIntegratedForm); module.exports = router; \ No newline at end of file diff --git a/saveICMdataHandler.js b/saveICMdataHandler.js index 748e04e..9834e0e 100644 --- a/saveICMdataHandler.js +++ b/saveICMdataHandler.js @@ -19,6 +19,10 @@ async function getICMAttachmentStatus(attachment_id, username, params) { return_data["Locked by Id"] = ""; return_data["DocFileName"] = ""; return_data["Office Name"] = ""; + return_data["Template"] = ""; + return_data["Tool"] = ""; + return_data["Categorie"] = ""; + if (!attachment_id || attachment_id == "") { return return_data; } @@ -45,6 +49,9 @@ async function getICMAttachmentStatus(attachment_id, username, params) { return_data["Locked by Id"] = response.data["Locked by Id"]; return_data["DocFileName"] = response.data["DocFileName"]; return_data["Office Name"] = response.data["Office Name"]; + return_data["Template"] = response.data["Template"]; + return_data["Tool"] = response.data["Tool"]; + return_data["Categorie"] = response.data["Categorie"]; return return_data; } catch (error) { @@ -126,8 +133,30 @@ async function saveICMdata(req, res) { saveJson["DocFileExt"] = "json"; saveJson["Doc Attachment Id"] = Buffer.from(savedFormParam).toString('base64');//savedForm is saved as attachment let saveData = JSON.parse(savedFormParam)["data"];// This is the data part of the savedJson - let builder = new xml2js.Builder(); - saveJson["XML Hierarchy"] = builder.buildObject(saveData); + const truncatedKeysSaveData = {}; + for(let oldKey in saveData) { //This begins trunicating the JSON keys for XML (UUID should be first 8 characters) + const stringLength = oldKey.length; + const newKey = oldKey.substring(0, stringLength-28); + if (Array.isArray(saveData[oldKey]) > 0 && Object.keys(saveData[oldKey]).length > 0) { //This trunicates child/dependant objects + const childrenArray = []; + for(let i = 0; i < saveData[oldKey].length; i++) { + const truncatedChildrenKeys = {}; + for (let oldChildKey in saveData[oldKey][i]) { + const childStringLength = oldChildKey.length; + const newChildKey = oldChildKey.substring(stringLength+3, childStringLength-28); + truncatedChildrenKeys[newChildKey] = saveData[oldKey][i][oldChildKey]; + } + childrenArray.push(truncatedChildrenKeys); + } + const wrapperKey = {} + wrapperKey[newKey] = childrenArray; + truncatedKeysSaveData[`${newKey}-List`] = wrapperKey // Add a wrapper around the children/dependecies + } else { + truncatedKeysSaveData[newKey] = saveData[oldKey]; //Data is added to new JSON with the truncated key + } + } + let builder = new xml2js.Builder({xmldec: { version: '1.0' }}); + saveJson["XML Hierarchy"] = builder.buildObject(truncatedKeysSaveData); //let url = buildUrlWithParams('SIEBEL_ICM_API_HOST', 'fwd/v1.0/data/DT Form Instance Thin/DT Form Instance Thin/' + attachment_id + '/', ''); let url = buildUrlWithParams(params["apiHost"], params["saveEndpoint"] + attachment_id + '/', params); try { diff --git a/schema/form_definition.yaml b/schema/form_definition.yaml index 392badb..d988ba6 100644 --- a/schema/form_definition.yaml +++ b/schema/form_definition.yaml @@ -7,11 +7,11 @@ required: properties: version: anyOf: - - type: string - - type: number + - type: string + - type: number ministry_id: anyOf: - - type: string + - type: string - type: number id: type: string @@ -25,13 +25,13 @@ properties: type: string deployed_to: anyOf: - - type: string + - type: string - type: "null" dataSources: type: array items: type: object - default: [] + default: [] data: type: object required: @@ -64,8 +64,8 @@ definitions: type: string mask: anyOf: - - type: string - - type: "null" + - type: string + - type: "null" codeContext: type: object properties: @@ -90,7 +90,7 @@ definitions: type: string required: - text - - value + - value groupItems: type: array items: @@ -101,7 +101,7 @@ definitions: items: $ref: "#/definitions/Item" repeater: - type: boolean + type: boolean helperText: anyOf: - type: string @@ -142,7 +142,7 @@ definitions: value: anyOf: - type: string - - type: number + - type: number - type: boolean errorMessage: anyOf: @@ -151,7 +151,7 @@ definitions: required: - type - value - - errorMessage + - errorMessage conditions: type: array items: @@ -169,30 +169,67 @@ definitions: required: - type - value - additionalProperties: false + additionalProperties: false webStyles: anyOf: - type: "null" - type: object - additionalProperties: - type: string + additionalProperties: + anyOf: + - type: string + - type: number + - type: boolean + - type: object + - type: array + - type: "null" pdfStyles: anyOf: - type: "null" - type: object - additionalProperties: - type: string + additionalProperties: + anyOf: + - type: string + - type: number + - type: boolean + - type: object + - type: array + - type: "null" databindings: - type: object - properties: - path: - type: string - source: - type: string - required: - - path - - source - additionalProperties: false + oneOf: + - type: array + items: + type: object + properties: + path: + type: string + source: + type: string + order: + type: number + condition: + anyOf: + - type: string + - type: "null" + required: + - path + - source + additionalProperties: false + - type: object + properties: + path: + type: string + source: + type: string + order: + type: number + condition: + anyOf: + - type: string + - type: "null" + required: + - path + - source + additionalProperties: false containerItems: type: array items: diff --git a/schema/saved_json.yaml b/schema/saved_json.yaml index 7712db5..6011102 100644 --- a/schema/saved_json.yaml +++ b/schema/saved_json.yaml @@ -1,3 +1,4 @@ + type: object required: - data @@ -13,7 +14,6 @@ properties: - type: number - type: object - type: array - - type: "null" items: type: object additionalProperties: @@ -23,6 +23,7 @@ properties: - type: number - type: object - type: "null" + - type: "null" form_definition: $ref: "form_definition_schema" metadata: