diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index a96091d1..44922c9c 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -1403,3 +1403,37 @@ Validation: Follow-ups: - Next slice should target non-proxy file handlers that still return raw `error` payloads. + +--- + +### CL-038: TASK22102 API contract consistency — Slice 4 (security hardening + larger non-proxy file set) + +date: 2026-03-17 +author: Cline +scope: `pages/api/middleware/apiResponse.js`, `pages/api/file/{createappealcompletemessage_api,updatecase_api,createcaseinvolvement_api,createrepinvolvement_api,createcase_api,generatepdf}.js` +type: change +rationale: Apply requested security hardening for error detail exposure and deliver a larger non-proxy file-handler slice to improve codex usage efficiency while keeping contract risk low. +impact: Suppresses sensitive `details` payloads in production, improves structured API consistency in selected non-proxy file handlers, and preserves existing success-path payload compatibility. +status: completed + +Summary: + +- Hardened shared error helper: + - `respondError` now omits `error.details` when `NODE_ENV === "production"`. + - debug detail visibility remains available in non-production environments. +- Migrated a larger batch of non-proxy file handlers to helper-based responses: + - `createappealcompletemessage_api` + - `updatecase_api` + - `createcaseinvolvement_api` + - `createrepinvolvement_api` + - `createcase_api` + - `generatepdf` +- Standardized missing-input/hash/failure paths to structured `respondError` codes/messages while retaining success bodies and existing 200 semantics (including 412->record exists handling in involvement endpoints). + +Validation: + +- `npx next lint --file pages/api/middleware/apiResponse.js --file pages/api/file/createappealcompletemessage_api.js --file pages/api/file/updatecase_api.js --file pages/api/file/createcaseinvolvement_api.js --file pages/api/file/createrepinvolvement_api.js --file pages/api/file/createcase_api.js --file pages/api/file/generatepdf.js` -> pass (no warnings/errors) + +Follow-ups: + +- Continue next larger slice on remaining non-proxy file handlers still returning raw error payloads. diff --git a/pages/api/file/createappealcompletemessage_api.js b/pages/api/file/createappealcompletemessage_api.js index 2c96b1a3..7b211519 100644 --- a/pages/api/file/createappealcompletemessage_api.js +++ b/pages/api/file/createappealcompletemessage_api.js @@ -12,6 +12,7 @@ import _ from "lodash"; import nextConnect from "next-connect"; import middleware from "../middleware/middleware"; +import { respondError, respondSuccess } from "../middleware/apiResponse"; const ApiProxy = nextConnect(); ApiProxy.use(middleware); @@ -30,7 +31,11 @@ ApiProxy.get(async (req, res) => { typeof checkHash === "undefined" || checkHash.length === 0 ) { - return res.status(400).json(); + return respondError(res, { + status: 400, + code: "MISSING_REQUIRED_QUERY", + message: "container, tempcaseref and hash are required" + }); } var checkquerypath = @@ -40,7 +45,11 @@ ApiProxy.get(async (req, res) => { tempCaseRef; if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { - return res.status(400).json(); + return respondError(res, { + status: 400, + code: "INVALID_HASH", + message: "Invalid hash" + }); } const blobProgress = await getProgressBlobs( @@ -107,14 +116,18 @@ ApiProxy.get(async (req, res) => { } createCaseCompleteMessage(containerName, tempCaseRef); - return res.status(200).json({ + return respondSuccess(res, { status: "success" }); }); }) .catch((error) => { consoleLogger(error); - return res.status(400).json(error); + return respondError(res, { + status: 400, + code: "CREATE_APPEAL_COMPLETE_MESSAGE_FAILED", + message: "Failed to create appeal complete message" + }); }); }); diff --git a/pages/api/file/createcase_api.js b/pages/api/file/createcase_api.js index 4a193f2a..95360e26 100644 --- a/pages/api/file/createcase_api.js +++ b/pages/api/file/createcase_api.js @@ -8,6 +8,7 @@ import { getCaseBlob, createBlob } from "../../../actions/azurestorage"; import { getTempCaseRef } from "../../../components/utils"; import { v4 as uuidv4 } from "uuid"; import { hashAPIPath } from "../../../actions/core/hash"; +import { respondError, respondSuccess } from "../middleware/apiResponse"; const WEBAPI_URL = process.env.RELAY_ROOT || @@ -33,7 +34,12 @@ export default async function ApiProxy(req, res) { typeof lpaID === "undefined" || lpaID.length === 0 ) { - return res.status(400).json(); + return respondError(res, { + status: 400, + code: "MISSING_REQUIRED_QUERY", + message: + "contactid, appealTypeId, containername and lpaID are required" + }); } var data = { @@ -73,10 +79,14 @@ export default async function ApiProxy(req, res) { // data: JSON.stringify(newData), // }; var apiResponse = _.isEmpty(req.query) - ? res.status(400).json() + ? respondError(res, { + status: 400, + code: "MISSING_REQUIRED_QUERY", + message: "Query parameters are required" + }) : (getCaseBlob(containerName, tempCaseRef, newData), createBlob(JSON.stringify(newAppealObj), containerName), - res.status(200).json(newData)); + respondSuccess(res, newData)); return apiResponse; } diff --git a/pages/api/file/createcaseinvolvement_api.js b/pages/api/file/createcaseinvolvement_api.js index a34d5fc6..af6b7848 100644 --- a/pages/api/file/createcaseinvolvement_api.js +++ b/pages/api/file/createcaseinvolvement_api.js @@ -15,6 +15,7 @@ import CryptoJS from "crypto-js"; import { consoleLogger } from "../../../actions/core/logger"; import { getToken } from "../../../actions/core/token"; import { hashAPIPath } from "../../../actions/core/hash"; +import { respondError, respondSuccess } from "../middleware/apiResponse"; const WEBAPI_URL = process.env.RELAY_ROOT || @@ -28,7 +29,11 @@ export default async function ApiProxy(req, res) { typeof req.body.incidentid === "undefined" || req.body.incidentid.length === 0 ) { - return res.status(400).json(); + return respondError(res, { + status: 400, + code: "MISSING_REQUIRED_BODY", + message: "contactid and incidentid are required" + }); } var token = await getToken(); @@ -61,12 +66,16 @@ export default async function ApiProxy(req, res) { return axios(config) .then(({ data }) => { - res.status(200).json(data); + return respondSuccess(res, data); }) .catch((error) => { return error.status == 412 - ? res.status(200).json({ "record": "exists" }) + ? respondSuccess(res, { "record": "exists" }) : (consoleLogger(Object.assign(error, data)), - res.status(400).json(error)); + respondError(res, { + status: 400, + code: "CREATE_CASE_INVOLVEMENT_FAILED", + message: "Failed to create case involvement" + })); }); } diff --git a/pages/api/file/createrepinvolvement_api.js b/pages/api/file/createrepinvolvement_api.js index 24349365..4f40349f 100644 --- a/pages/api/file/createrepinvolvement_api.js +++ b/pages/api/file/createrepinvolvement_api.js @@ -15,6 +15,7 @@ import CryptoJS from "crypto-js"; import { consoleLogger } from "../../../actions/core/logger"; import { getToken } from "../../../actions/core/token"; import { hashAPIPath } from "../../../actions/core/hash"; +import { respondError, respondSuccess } from "../middleware/apiResponse"; const WEBAPI_URL = process.env.RELAY_ROOT || @@ -28,7 +29,11 @@ export default async function ApiProxy(req, res) { typeof req.body.incidentid === "undefined" || req.body.incidentid.length === 0 ) { - return res.status(400).json(); + return respondError(res, { + status: 400, + code: "MISSING_REQUIRED_BODY", + message: "contactid and incidentid are required" + }); } var token = await getToken(); @@ -63,12 +68,16 @@ export default async function ApiProxy(req, res) { return axios(config) .then(({ data }) => { - res.status(200).json(data); + return respondSuccess(res, data); }) .catch((error) => { return error.status == 412 - ? res.status(200).json({ "record": "exists" }) + ? respondSuccess(res, { "record": "exists" }) : (consoleLogger(Object.assign(error, data)), - res.status(400).json(error)); + respondError(res, { + status: 400, + code: "CREATE_REP_INVOLVEMENT_FAILED", + message: "Failed to create representation involvement" + })); }); } diff --git a/pages/api/file/generatepdf.js b/pages/api/file/generatepdf.js index 58624ce7..981d4f0e 100644 --- a/pages/api/file/generatepdf.js +++ b/pages/api/file/generatepdf.js @@ -101,6 +101,7 @@ import { statement_pdf } from "../../../components/pdftemplates/statement_pdf"; import { writtenStatement_pdf } from "../../../components/pdftemplates/writtenStatement_pdf"; import { other_pdf } from "../../../components/pdftemplates/other_pdf"; import { consoleLogger } from "../../../actions/core/logger"; +import { respondError, respondSuccess } from "../middleware/apiResponse"; //const ApiProxy = nextConnect(); // ApiProxy.use(middleware); @@ -183,14 +184,22 @@ export default async function handler(req, res) { } if (typeof checkHash === "undefined" || checkHash.length === 0) { - return res.status(400).json(); + return respondError(res, { + status: 400, + code: "HASH_REQUIRED", + message: "hash is required" + }); } if ( hashAPIPath(checkquerypath) != (checkquerypath.indexOf("?") > -1 ? "&hash=" : "?hash=") + checkHash ) { - return res.status(400).json(); + return respondError(res, { + status: 400, + code: "INVALID_HASH", + message: "Invalid hash" + }); } //console.log("-------", checkHash); @@ -349,7 +358,7 @@ export default async function handler(req, res) { return res.status(200).send(renderedPDFBuffer); } - return res.status(200).json({ + return respondSuccess(res, { status: "success", data: data, path: `${caseRef}/${reqBodyobj.repfile_name}/files/${reqBodyobj.repfile_name}.pdf` @@ -357,6 +366,10 @@ export default async function handler(req, res) { }) .catch((error) => { consoleLogger(error); - res.status(400).json(error); + return respondError(res, { + status: 400, + code: "GENERATE_PDF_FAILED", + message: "Failed to generate PDF" + }); }); } diff --git a/pages/api/file/updatecase_api.js b/pages/api/file/updatecase_api.js index 37936335..c458b8dd 100644 --- a/pages/api/file/updatecase_api.js +++ b/pages/api/file/updatecase_api.js @@ -3,6 +3,7 @@ import CryptoJS from "crypto-js"; import { consoleLogger } from "../../../actions/core/logger"; import { getToken } from "../../../actions/core/token"; import { hashAPIPath } from "../../../actions/core/hash"; +import { respondError, respondSuccess } from "../middleware/apiResponse"; const WEBAPI_URL = process.env.RELAY_ROOT || @@ -26,7 +27,12 @@ export default async function ApiProxy(req, res) { typeof req.body === "undefined" || req.body === null ) { - return res.status(400).json(); + return respondError(res, { + status: 400, + code: "MISSING_REQUIRED_QUERY", + message: + "updateFormCollection, appealObj, incident and body are required" + }); } var token = await getToken(); @@ -49,10 +55,14 @@ export default async function ApiProxy(req, res) { return axios(config) .then(({ data }) => { - res.status(200).json(data); + return respondSuccess(res, data); }) .catch((error) => { consoleLogger(error); - res.status(400).json(error); + return respondError(res, { + status: 400, + code: "UPDATE_CASE_FAILED", + message: "Failed to update case" + }); }); } diff --git a/pages/api/middleware/apiResponse.js b/pages/api/middleware/apiResponse.js index d6163e4f..52d2f14a 100644 --- a/pages/api/middleware/apiResponse.js +++ b/pages/api/middleware/apiResponse.js @@ -11,6 +11,7 @@ export const respondError = ( details } = {} ) => { + const isProduction = process.env.NODE_ENV === "production"; const payload = { success: false, error: { @@ -19,7 +20,7 @@ export const respondError = ( } }; - if (typeof details !== "undefined") { + if (!isProduction && typeof details !== "undefined") { payload.error.details = details; }