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:
- 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 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"
});
});
});
+13 -3
View File
@@ -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;
}
+13 -4
View File
@@ -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"
}));
});
}
+13 -4
View File
@@ -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"
}));
});
}
+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 { 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"
});
});
}
+13 -3
View File
@@ -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"
});
});
}
+2 -1
View File
@@ -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;
}