TASK22102: slice4 larger file set + production detail suppression

This commit is contained in:
2026-03-17 13:06:28 +00:00
parent 3cbd876b77
commit e68319f16c
8 changed files with 122 additions and 23 deletions
+34
View File
@@ -1403,3 +1403,37 @@ Validation:
Follow-ups: Follow-ups:
- Next slice should target non-proxy file handlers that still return raw `error` payloads. - 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.
@@ -12,6 +12,7 @@ import _ from "lodash";
import nextConnect from "next-connect"; import nextConnect from "next-connect";
import middleware from "../middleware/middleware"; import middleware from "../middleware/middleware";
import { respondError, respondSuccess } from "../middleware/apiResponse";
const ApiProxy = nextConnect(); const ApiProxy = nextConnect();
ApiProxy.use(middleware); ApiProxy.use(middleware);
@@ -30,7 +31,11 @@ ApiProxy.get(async (req, res) => {
typeof checkHash === "undefined" || typeof checkHash === "undefined" ||
checkHash.length === 0 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 = var checkquerypath =
@@ -40,7 +45,11 @@ ApiProxy.get(async (req, res) => {
tempCaseRef; tempCaseRef;
if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { 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( const blobProgress = await getProgressBlobs(
@@ -107,14 +116,18 @@ ApiProxy.get(async (req, res) => {
} }
createCaseCompleteMessage(containerName, tempCaseRef); createCaseCompleteMessage(containerName, tempCaseRef);
return res.status(200).json({ return respondSuccess(res, {
status: "success" status: "success"
}); });
}); });
}) })
.catch((error) => { .catch((error) => {
consoleLogger(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"
});
}); });
}); });
+13 -3
View File
@@ -8,6 +8,7 @@ import { getCaseBlob, createBlob } from "../../../actions/azurestorage";
import { getTempCaseRef } from "../../../components/utils"; import { getTempCaseRef } from "../../../components/utils";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { hashAPIPath } from "../../../actions/core/hash"; import { hashAPIPath } from "../../../actions/core/hash";
import { respondError, respondSuccess } from "../middleware/apiResponse";
const WEBAPI_URL = const WEBAPI_URL =
process.env.RELAY_ROOT || process.env.RELAY_ROOT ||
@@ -33,7 +34,12 @@ export default async function ApiProxy(req, res) {
typeof lpaID === "undefined" || typeof lpaID === "undefined" ||
lpaID.length === 0 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 = { var data = {
@@ -73,10 +79,14 @@ export default async function ApiProxy(req, res) {
// data: JSON.stringify(newData), // data: JSON.stringify(newData),
// }; // };
var apiResponse = _.isEmpty(req.query) 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), : (getCaseBlob(containerName, tempCaseRef, newData),
createBlob(JSON.stringify(newAppealObj), containerName), createBlob(JSON.stringify(newAppealObj), containerName),
res.status(200).json(newData)); respondSuccess(res, newData));
return apiResponse; return apiResponse;
} }
+13 -4
View File
@@ -15,6 +15,7 @@ import CryptoJS from "crypto-js";
import { consoleLogger } from "../../../actions/core/logger"; import { consoleLogger } from "../../../actions/core/logger";
import { getToken } from "../../../actions/core/token"; import { getToken } from "../../../actions/core/token";
import { hashAPIPath } from "../../../actions/core/hash"; import { hashAPIPath } from "../../../actions/core/hash";
import { respondError, respondSuccess } from "../middleware/apiResponse";
const WEBAPI_URL = const WEBAPI_URL =
process.env.RELAY_ROOT || process.env.RELAY_ROOT ||
@@ -28,7 +29,11 @@ export default async function ApiProxy(req, res) {
typeof req.body.incidentid === "undefined" || typeof req.body.incidentid === "undefined" ||
req.body.incidentid.length === 0 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(); var token = await getToken();
@@ -61,12 +66,16 @@ export default async function ApiProxy(req, res) {
return axios(config) return axios(config)
.then(({ data }) => { .then(({ data }) => {
res.status(200).json(data); return respondSuccess(res, data);
}) })
.catch((error) => { .catch((error) => {
return error.status == 412 return error.status == 412
? res.status(200).json({ "record": "exists" }) ? respondSuccess(res, { "record": "exists" })
: (consoleLogger(Object.assign(error, data)), : (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"
}));
}); });
} }
+13 -4
View File
@@ -15,6 +15,7 @@ import CryptoJS from "crypto-js";
import { consoleLogger } from "../../../actions/core/logger"; import { consoleLogger } from "../../../actions/core/logger";
import { getToken } from "../../../actions/core/token"; import { getToken } from "../../../actions/core/token";
import { hashAPIPath } from "../../../actions/core/hash"; import { hashAPIPath } from "../../../actions/core/hash";
import { respondError, respondSuccess } from "../middleware/apiResponse";
const WEBAPI_URL = const WEBAPI_URL =
process.env.RELAY_ROOT || process.env.RELAY_ROOT ||
@@ -28,7 +29,11 @@ export default async function ApiProxy(req, res) {
typeof req.body.incidentid === "undefined" || typeof req.body.incidentid === "undefined" ||
req.body.incidentid.length === 0 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(); var token = await getToken();
@@ -63,12 +68,16 @@ export default async function ApiProxy(req, res) {
return axios(config) return axios(config)
.then(({ data }) => { .then(({ data }) => {
res.status(200).json(data); return respondSuccess(res, data);
}) })
.catch((error) => { .catch((error) => {
return error.status == 412 return error.status == 412
? res.status(200).json({ "record": "exists" }) ? respondSuccess(res, { "record": "exists" })
: (consoleLogger(Object.assign(error, data)), : (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"
}));
}); });
} }
+17 -4
View File
@@ -101,6 +101,7 @@ import { statement_pdf } from "../../../components/pdftemplates/statement_pdf";
import { writtenStatement_pdf } from "../../../components/pdftemplates/writtenStatement_pdf"; import { writtenStatement_pdf } from "../../../components/pdftemplates/writtenStatement_pdf";
import { other_pdf } from "../../../components/pdftemplates/other_pdf"; import { other_pdf } from "../../../components/pdftemplates/other_pdf";
import { consoleLogger } from "../../../actions/core/logger"; import { consoleLogger } from "../../../actions/core/logger";
import { respondError, respondSuccess } from "../middleware/apiResponse";
//const ApiProxy = nextConnect(); //const ApiProxy = nextConnect();
// ApiProxy.use(middleware); // ApiProxy.use(middleware);
@@ -183,14 +184,22 @@ export default async function handler(req, res) {
} }
if (typeof checkHash === "undefined" || checkHash.length === 0) { 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 ( if (
hashAPIPath(checkquerypath) != hashAPIPath(checkquerypath) !=
(checkquerypath.indexOf("?") > -1 ? "&hash=" : "?hash=") + checkHash (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); //console.log("-------", checkHash);
@@ -349,7 +358,7 @@ export default async function handler(req, res) {
return res.status(200).send(renderedPDFBuffer); return res.status(200).send(renderedPDFBuffer);
} }
return res.status(200).json({ return respondSuccess(res, {
status: "success", status: "success",
data: data, data: data,
path: `${caseRef}/${reqBodyobj.repfile_name}/files/${reqBodyobj.repfile_name}.pdf` path: `${caseRef}/${reqBodyobj.repfile_name}/files/${reqBodyobj.repfile_name}.pdf`
@@ -357,6 +366,10 @@ export default async function handler(req, res) {
}) })
.catch((error) => { .catch((error) => {
consoleLogger(error); consoleLogger(error);
res.status(400).json(error); return respondError(res, {
status: 400,
code: "GENERATE_PDF_FAILED",
message: "Failed to generate PDF"
});
}); });
} }
+13 -3
View File
@@ -3,6 +3,7 @@ import CryptoJS from "crypto-js";
import { consoleLogger } from "../../../actions/core/logger"; import { consoleLogger } from "../../../actions/core/logger";
import { getToken } from "../../../actions/core/token"; import { getToken } from "../../../actions/core/token";
import { hashAPIPath } from "../../../actions/core/hash"; import { hashAPIPath } from "../../../actions/core/hash";
import { respondError, respondSuccess } from "../middleware/apiResponse";
const WEBAPI_URL = const WEBAPI_URL =
process.env.RELAY_ROOT || process.env.RELAY_ROOT ||
@@ -26,7 +27,12 @@ export default async function ApiProxy(req, res) {
typeof req.body === "undefined" || typeof req.body === "undefined" ||
req.body === null 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(); var token = await getToken();
@@ -49,10 +55,14 @@ export default async function ApiProxy(req, res) {
return axios(config) return axios(config)
.then(({ data }) => { .then(({ data }) => {
res.status(200).json(data); return respondSuccess(res, data);
}) })
.catch((error) => { .catch((error) => {
consoleLogger(error); consoleLogger(error);
res.status(400).json(error); return respondError(res, {
status: 400,
code: "UPDATE_CASE_FAILED",
message: "Failed to update case"
});
}); });
} }
+2 -1
View File
@@ -11,6 +11,7 @@ export const respondError = (
details details
} = {} } = {}
) => { ) => {
const isProduction = process.env.NODE_ENV === "production";
const payload = { const payload = {
success: false, success: false,
error: { error: {
@@ -19,7 +20,7 @@ export const respondError = (
} }
}; };
if (typeof details !== "undefined") { if (!isProduction && typeof details !== "undefined") {
payload.error.details = details; payload.error.details = details;
} }