From 9ae8ce7136394eb19bce55209178788d5a1518b9 Mon Sep 17 00:00:00 2001 From: robbond Date: Tue, 17 Mar 2026 12:50:54 +0000 Subject: [PATCH 1/7] TASK22102: slice1 api contract helper + email/admin pilot --- memory-bank/activeContext.md | 1 + memory-bank/change-log.md | 37 +++++++++++++++++++++++ pages/api/admin/getlatestdocuments_api.js | 9 ++++-- pages/api/admin/getnewappeals_api.js | 9 ++++-- pages/api/email/getmailinglist.js | 9 ++++-- pages/api/email/notify.js | 15 +++++++-- pages/api/middleware/apiResponse.js | 27 +++++++++++++++++ 7 files changed, 98 insertions(+), 9 deletions(-) create mode 100644 pages/api/middleware/apiResponse.js diff --git a/memory-bank/activeContext.md b/memory-bank/activeContext.md index 6b28e99d..6dc925c7 100644 --- a/memory-bank/activeContext.md +++ b/memory-bank/activeContext.md @@ -2,6 +2,7 @@ ## Current development focus (from recent commits) +- API contract consistency rollout (TASK22102), starting with helper-based response normalization in selected email/admin handlers. - Search/case navigation correctness, especially breadcrumb and back-link behavior. - My Portal "view all" and DNS application path handling. - Welsh/English email behavior for specific notification templates. diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index 2246687d..300c2dec 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -1302,3 +1302,40 @@ Validation: Follow-ups: - Continue same small-batch consistency pattern for remaining endpoint handlers where required-input/logging drift is clear. + +--- + +### CL-035: TASK22102 API contract consistency — Slice 1 (response helper + email/admin pilot) + +date: 2026-03-17 +author: Cline +scope: `pages/api/middleware/apiResponse.js`, `pages/api/email/{notify,getmailinglist}.js`, `pages/api/admin/{getnewappeals_api,getlatestdocuments_api}.js`, `memory-bank/*` +type: change +rationale: Start API contract-consistency program with a low-risk pilot slice introducing shared response helpers and replacing raw error passthrough in selected email/admin handlers. +impact: Improves response contract consistency and prevents raw error payload leakage in pilot handlers while preserving existing success-body compatibility. +status: completed + +Summary: + +- Added shared API response helper module: + - `pages/api/middleware/apiResponse.js` + - `respondSuccess(res, data, status)` + - `respondError(res, { status, code, message, details })` +- Migrated first slice handlers to helper-based responses: + - `pages/api/email/notify.js` + - `pages/api/email/getmailinglist.js` + - `pages/api/admin/getnewappeals_api.js` + - `pages/api/admin/getlatestdocuments_api.js` +- Contract updates in slice: + - replaced `res.status(400).json(error)` with structured error envelope + - replaced direct `res.status(200).json(...)` with `respondSuccess(...)` + - kept success payload shape as existing data object for non-breaking rollout + +Validation: + +- `npx next lint --file pages/api/middleware/apiResponse.js --file pages/api/email/notify.js --file pages/api/email/getmailinglist.js --file pages/api/admin/getnewappeals_api.js --file pages/api/admin/getlatestdocuments_api.js` -> pass (no warnings/errors) + +Follow-ups: + +- Slice 2 should migrate remaining email handlers (`getcaseref`, `getall`, `getevents`, `getdocuments`) to helper responses. +- After email/admin completion, expand to selected high-risk file handlers using same non-breaking helper model. diff --git a/pages/api/admin/getlatestdocuments_api.js b/pages/api/admin/getlatestdocuments_api.js index d9b6b622..686352b3 100644 --- a/pages/api/admin/getlatestdocuments_api.js +++ b/pages/api/admin/getlatestdocuments_api.js @@ -22,6 +22,7 @@ import { azureHeadersPagedCustom } from "../../../actions/core/headers"; 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 WORDKEY = process.env.HASHKEY; const WEBAPI_URL = @@ -154,11 +155,15 @@ export default async function ApiProxy(req, res) { _.has(data, "@odata.nextLink") == true && ((dataStr = JSON.stringify(data["@odata.nextLink"])), (data["@odata.nextLink"] = dataStr.split("/v8.2/")[1])); - res.status(200).json(data); + return respondSuccess(res, data); }) .catch((error) => { consoleLogger(error); - res.status(400).json(error); + return respondError(res, { + status: 400, + code: "ADMIN_LATEST_DOCS_FETCH_FAILED", + message: "Failed to fetch latest documents" + }); }); return apiResponse; diff --git a/pages/api/admin/getnewappeals_api.js b/pages/api/admin/getnewappeals_api.js index 2ab64f48..595ed467 100644 --- a/pages/api/admin/getnewappeals_api.js +++ b/pages/api/admin/getnewappeals_api.js @@ -22,6 +22,7 @@ import { azureHeadersPagedCustom } from "../../../actions/core/headers"; import { getToken } from "../../../actions/core/token"; import { consoleLogger } from "../../../actions/core/logger"; import { hashAPIPath } from "../../../actions/core/hash"; +import { respondError, respondSuccess } from "../middleware/apiResponse"; const WEBAPI_URL = process.env.RELAY_ROOT || @@ -64,11 +65,15 @@ export default async function ApiProxy(req, res) { _.has(data, "@odata.nextLink") == true && ((dataStr = JSON.stringify(data["@odata.nextLink"])), (data["@odata.nextLink"] = dataStr.split("/v8.2/")[1])); - res.status(200).json(data); + return respondSuccess(res, data); }) .catch((error) => { consoleLogger(error); - res.status(400).json(error); + return respondError(res, { + status: 400, + code: "ADMIN_NEW_APPEALS_FETCH_FAILED", + message: "Failed to fetch new appeals" + }); }); return apiResponse; diff --git a/pages/api/email/getmailinglist.js b/pages/api/email/getmailinglist.js index 3aaebd3d..44ff7297 100644 --- a/pages/api/email/getmailinglist.js +++ b/pages/api/email/getmailinglist.js @@ -5,6 +5,7 @@ import { azureHeaders } from "../../../actions/core/headers"; 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 WORDKEY = process.env.HASHKEY; @@ -40,10 +41,14 @@ export default async function ApiProxy(req, res) { const flattenedResults = data.value.map(flattenWatchlistEntry); - res.status(200).json(flattenedResults); + return respondSuccess(res, flattenedResults); }) .catch((error) => { consoleLogger(error); - res.status(400).json(error); + return respondError(res, { + status: 400, + code: "MAILING_LIST_FETCH_FAILED", + message: "Failed to fetch mailing list" + }); }); } diff --git a/pages/api/email/notify.js b/pages/api/email/notify.js index c74edcc4..a9cef216 100644 --- a/pages/api/email/notify.js +++ b/pages/api/email/notify.js @@ -28,13 +28,18 @@ import { consoleLogger, redactSensitive } from "../../../actions/core/logger"; import { isNonEmptyString, sanitizeString } from "../../../actions/core/guards"; import { getPreferredLanguage } from "../../../actions/services/accountService"; +import { respondError, respondSuccess } from "../middleware/apiResponse"; export default async function ApiProxy(req, res) { var data = req.body; const emailAddress = sanitizeString(data?.emailAddress); if (!isNonEmptyString(emailAddress)) { - return res.status(400).json({ error: "emailAddress is required" }); + return respondError(res, { + status: 400, + code: "EMAIL_ADDRESS_REQUIRED", + message: "emailAddress is required" + }); } data.emailAddress = emailAddress; @@ -71,11 +76,15 @@ export default async function ApiProxy(req, res) { }) .then((response) => { //console.log("thisis the response", response); - return res.status(200).json(data); + return respondSuccess(res, data); }) .catch((error) => { consoleLogger(error); - return res.status(400).json(error); + return respondError(res, { + status: 400, + code: "EMAIL_NOTIFY_FAILED", + message: "Failed to send notify email" + }); }); //return res.status(200).json(data); } diff --git a/pages/api/middleware/apiResponse.js b/pages/api/middleware/apiResponse.js new file mode 100644 index 00000000..d6163e4f --- /dev/null +++ b/pages/api/middleware/apiResponse.js @@ -0,0 +1,27 @@ +export const respondSuccess = (res, data, status = 200) => { + return res.status(status).json(data); +}; + +export const respondError = ( + res, + { + status = 500, + code = "INTERNAL_SERVER_ERROR", + message = "Request failed", + details + } = {} +) => { + const payload = { + success: false, + error: { + code, + message + } + }; + + if (typeof details !== "undefined") { + payload.error.details = details; + } + + return res.status(status).json(payload); +}; From f16161bcee15ab25a002a0c5d12f52e3cbdb927c Mon Sep 17 00:00:00 2001 From: robbond Date: Tue, 17 Mar 2026 12:55:05 +0000 Subject: [PATCH 2/7] TASK22102: slice2 migrate remaining email handlers --- memory-bank/change-log.md | 32 ++++++++++++++++++++++++++++++++ pages/api/email/getall.js | 9 ++++++--- pages/api/email/getcaseref.js | 10 +++++++--- pages/api/email/getdocuments.js | 17 +++++++++++------ pages/api/email/getevents.js | 23 ++++++++++++++++------- 5 files changed, 72 insertions(+), 19 deletions(-) diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index 300c2dec..b3268476 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -1339,3 +1339,35 @@ Follow-ups: - Slice 2 should migrate remaining email handlers (`getcaseref`, `getall`, `getevents`, `getdocuments`) to helper responses. - After email/admin completion, expand to selected high-risk file handlers using same non-breaking helper model. + +--- + +### CL-036: TASK22102 API contract consistency — Slice 2 (remaining email handlers) + +date: 2026-03-17 +author: Cline +scope: `pages/api/email/{getcaseref,getall,getevents,getdocuments}.js` +type: change +rationale: Complete the email-domain response-contract consistency rollout by moving remaining handlers to shared helper-based success/error responses. +impact: Removes raw error passthrough in remaining email handlers and standardizes structured error envelopes while preserving success payload compatibility. +status: completed + +Summary: + +- Migrated remaining email handlers to shared response helper usage: + - `getcaseref` + - `getall` + - `getevents` + - `getdocuments` +- Replaced direct `res.status(...).json(...)` branches with: + - `respondSuccess(...)` + - `respondError(...)` +- Added stable error codes/messages for expected negative paths and catch branches. + +Validation: + +- `npx next lint --file pages/api/email/getcaseref.js --file pages/api/email/getevents.js --file pages/api/email/getdocuments.js --file pages/api/email/getall.js` -> pass (no warnings/errors) + +Follow-ups: + +- Next slice can target selected file handlers for equivalent response contract cleanup. diff --git a/pages/api/email/getall.js b/pages/api/email/getall.js index d423a303..e54812ac 100644 --- a/pages/api/email/getall.js +++ b/pages/api/email/getall.js @@ -6,6 +6,7 @@ import { consoleLogger } from "../../../actions/core/logger"; import { getToken } from "../../../actions/core/token"; import { formatDates } from "../../../components/utils"; import { hashAPIPath } from "../../../actions/core/hash"; +import { respondError, respondSuccess } from "../middleware/apiResponse"; var NotifyClient = require("notifications-node-client").NotifyClient; @@ -439,11 +440,13 @@ export default async function CombinedApiProxy(req, res) { sendingResults.push(result); } - res.status(200).json({ sendingResults }); + return respondSuccess(res, { sendingResults }); } catch (error) { consoleLogger(error); - res.status(500).json({ - error: "An error occurred while retrieving combined data.", + return respondError(res, { + status: 500, + code: "EMAIL_COMBINED_FETCH_FAILED", + message: "An error occurred while retrieving combined data.", details: error.message || error.toString() }); } diff --git a/pages/api/email/getcaseref.js b/pages/api/email/getcaseref.js index ab259902..b4aa179d 100644 --- a/pages/api/email/getcaseref.js +++ b/pages/api/email/getcaseref.js @@ -5,6 +5,7 @@ import { azureHeaders } from "../../../actions/core/headers"; 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 || @@ -23,7 +24,6 @@ function flattenWatchlistEntry(entry) { } export default async function ApiProxy(req, res) { - const incidentID = req.query.incidentid; var token = await getToken(); var queryUrl = @@ -39,10 +39,14 @@ export default async function ApiProxy(req, res) { const flattenedResults = data.value.map(flattenWatchlistEntry); - res.status(200).json(flattenedResults); + return respondSuccess(res, flattenedResults); }) .catch((error) => { consoleLogger(error); - res.status(400).json(error); + return respondError(res, { + status: 400, + code: "CASE_REF_FETCH_FAILED", + message: "Failed to fetch case references" + }); }); } diff --git a/pages/api/email/getdocuments.js b/pages/api/email/getdocuments.js index 781d74cb..3caa7ef7 100644 --- a/pages/api/email/getdocuments.js +++ b/pages/api/email/getdocuments.js @@ -5,6 +5,7 @@ import { azureHeadersPaged } from "../../../actions/core/headers"; 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 WORDKEY = process.env.HASHKEY; const WEBAPI_URL = @@ -26,9 +27,11 @@ export default async function ApiProxy(req, res) { const incidentID = req.query.incidentid; if (!incidentID) { - return res - .status(400) - .json({ error: "incidentid query parameter is required." }); + return respondError(res, { + status: 400, + code: "INCIDENT_ID_REQUIRED", + message: "incidentid query parameter is required." + }); } try { @@ -79,11 +82,13 @@ export default async function ApiProxy(req, res) { ) })); - res.status(200).json({ ...data, value: resultsWithLinks }); + return respondSuccess(res, { ...data, value: resultsWithLinks }); } catch (error) { consoleLogger(error); - res.status(500).json({ - error: "Failed to fetch document details.", + return respondError(res, { + status: 500, + code: "DOCUMENTS_FETCH_FAILED", + message: "Failed to fetch document details.", details: error.message || error.toString() }); } diff --git a/pages/api/email/getevents.js b/pages/api/email/getevents.js index 75fc8e50..79c9feda 100644 --- a/pages/api/email/getevents.js +++ b/pages/api/email/getevents.js @@ -5,6 +5,7 @@ import { azureHeaders } from "../../../actions/core/headers"; 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 WORDKEY = process.env.HASHKEY; const WEBAPI_URL = @@ -18,7 +19,11 @@ export default async function ApiProxy(req, res) { const { incidentID } = req.query; if (!incidentID) { - return res.status(400).json({ error: "incidentID is required" }); + return respondError(res, { + status: 400, + code: "INCIDENT_ID_REQUIRED", + message: "incidentID is required" + }); } try { @@ -34,9 +39,11 @@ export default async function ApiProxy(req, res) { const sipsRecords = _.get(sipsResponse, "data.value", []); if (sipsRecords.length === 0) { - return res - .status(404) - .json({ error: "No SIPs record found for this incidentID." }); + return respondError(res, { + status: 404, + code: "SIPS_RECORD_NOT_FOUND", + message: "No SIPs record found for this incidentID." + }); } const sipsId = sipsRecords[0].pinswg_sipsid; @@ -53,11 +60,13 @@ export default async function ApiProxy(req, res) { azureHeaders(token.access_token) ); - res.status(200).json(eventsResponse.data); + return respondSuccess(res, eventsResponse.data); } catch (error) { consoleLogger(error); - res.status(500).json({ - error: "An error occurred while retrieving data.", + return respondError(res, { + status: 500, + code: "EVENTS_FETCH_FAILED", + message: "An error occurred while retrieving data.", details: error.message || error.toString() }); } From 3cbd876b779ba32664b958c3c857ce45d4d96f6b Mon Sep 17 00:00:00 2001 From: robbond Date: Tue, 17 Mar 2026 12:58:20 +0000 Subject: [PATCH 3/7] TASK22102: slice3 migrate file proxy handlers --- memory-bank/change-log.md | 32 +++++++++++++++++++ .../createappealcompletemessageproxy_api.js | 15 +++++++-- .../getawaitingsubmissionfromblobproxy.js | 15 +++++++-- pages/api/file/getbloblistproxy.js | 15 +++++++-- pages/api/file/getrepsblobproxy.js | 15 +++++++-- 5 files changed, 80 insertions(+), 12 deletions(-) diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index b3268476..a96091d1 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -1371,3 +1371,35 @@ Validation: Follow-ups: - Next slice can target selected file handlers for equivalent response contract cleanup. + +--- + +### CL-037: TASK22102 API contract consistency — Slice 3 (file proxy handlers) + +date: 2026-03-17 +author: Cline +scope: `pages/api/file/{getbloblistproxy,getrepsblobproxy,getawaitingsubmissionfromblobproxy,createappealcompletemessageproxy_api}.js` +type: change +rationale: Extend contract consistency into selected proxy file handlers by replacing raw error passthrough and empty 400s with standardized structured responses. +impact: Improves API error contract clarity for selected file proxy routes while preserving success payload pass-through behavior. +status: completed + +Summary: + +- Migrated selected file proxy handlers to shared response helper usage: + - `getbloblistproxy` + - `getrepsblobproxy` + - `getawaitingsubmissionfromblobproxy` + - `createappealcompletemessageproxy_api` +- Replaced direct `res.status(...).json(...)` handling with: + - `respondSuccess(...)` + - `respondError(...)` +- Added explicit structured 400 responses for missing required query values and proxy fetch failures. + +Validation: + +- `npx next lint --file pages/api/file/getbloblistproxy.js --file pages/api/file/getrepsblobproxy.js --file pages/api/file/getawaitingsubmissionfromblobproxy.js --file pages/api/file/createappealcompletemessageproxy_api.js` -> pass (no warnings/errors) + +Follow-ups: + +- Next slice should target non-proxy file handlers that still return raw `error` payloads. diff --git a/pages/api/file/createappealcompletemessageproxy_api.js b/pages/api/file/createappealcompletemessageproxy_api.js index 5ebbdb6c..b495040c 100644 --- a/pages/api/file/createappealcompletemessageproxy_api.js +++ b/pages/api/file/createappealcompletemessageproxy_api.js @@ -3,6 +3,7 @@ import { getToken } from "../../../actions/core/token"; import { azureHeaders } from "../../../actions/core/headers"; import { consoleLogger } from "../../../actions/core/logger"; import axios from "axios"; +import { respondError, respondSuccess } from "../middleware/apiResponse"; import nextConnect from "next-connect"; import middleware from "../middleware/middleware"; @@ -20,7 +21,11 @@ ApiProxy.get(async (req, res) => { var tempCaseRef = req.query.tempcaseref; if (!hasValue(containerName) || !hasValue(tempCaseRef)) { - return res.status(400).json(); + return respondError(res, { + status: 400, + code: "MISSING_REQUIRED_QUERY", + message: "container and tempcaseref are required" + }); } var token = await getToken(); @@ -37,11 +42,15 @@ ApiProxy.get(async (req, res) => { azureHeaders(token.access_token) ) .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: "CREATE_APPEAL_COMPLETE_MESSAGE_PROXY_FAILED", + message: "Failed to create appeal complete message" + }); }); }); diff --git a/pages/api/file/getawaitingsubmissionfromblobproxy.js b/pages/api/file/getawaitingsubmissionfromblobproxy.js index 6f8e3ef7..f04a2198 100644 --- a/pages/api/file/getawaitingsubmissionfromblobproxy.js +++ b/pages/api/file/getawaitingsubmissionfromblobproxy.js @@ -3,6 +3,7 @@ import { azureHeaders } from "../../../actions/core/headers"; import { consoleLogger } from "../../../actions/core/logger"; import { hashAPIPath } from "../../../actions/core/hash"; import axios from "axios"; +import { respondError, respondSuccess } from "../middleware/apiResponse"; const BASE_URL = process.env.API_ROOT || `http://localhost:${port}`; @@ -13,7 +14,11 @@ export default async function ApiProxy(req, res) { var containerName = req.query.container; if (!hasValue(containerName)) { - return res.status(400).json(); + return respondError(res, { + status: 400, + code: "MISSING_REQUIRED_QUERY", + message: "container is required" + }); } var token = await getToken(); @@ -27,10 +32,14 @@ export default async function ApiProxy(req, res) { azureHeaders(token.access_token) ) .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: "GET_AWAITING_SUBMISSION_PROXY_FAILED", + message: "Failed to fetch awaiting submission blob" + }); }); } diff --git a/pages/api/file/getbloblistproxy.js b/pages/api/file/getbloblistproxy.js index 10a7b958..c6771fca 100644 --- a/pages/api/file/getbloblistproxy.js +++ b/pages/api/file/getbloblistproxy.js @@ -3,6 +3,7 @@ import { azureHeaders } from "../../../actions/core/headers"; import { consoleLogger } from "../../../actions/core/logger"; import { hashAPIPath } from "../../../actions/core/hash"; import axios from "axios"; +import { respondError, respondSuccess } from "../middleware/apiResponse"; const BASE_URL = process.env.API_ROOT || `http://localhost:${port}`; @@ -14,7 +15,11 @@ export default async function ApiProxy(req, res) { var casefolderID = req.query.casefolderID; if (!hasValue(containerName) || !hasValue(casefolderID)) { - return res.status(400).json(); + return respondError(res, { + status: 400, + code: "MISSING_REQUIRED_QUERY", + message: "container and casefolderID are required" + }); } var token = await getToken(); @@ -31,10 +36,14 @@ export default async function ApiProxy(req, res) { azureHeaders(token.access_token) ) .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: "GET_BLOB_LIST_PROXY_FAILED", + message: "Failed to fetch blob list" + }); }); } diff --git a/pages/api/file/getrepsblobproxy.js b/pages/api/file/getrepsblobproxy.js index 3a0bb850..36383f1a 100644 --- a/pages/api/file/getrepsblobproxy.js +++ b/pages/api/file/getrepsblobproxy.js @@ -3,6 +3,7 @@ import { azureHeaders } from "../../../actions/core/headers"; import { consoleLogger } from "../../../actions/core/logger"; import { hashAPIPath } from "../../../actions/core/hash"; import axios from "axios"; +import { respondError, respondSuccess } from "../middleware/apiResponse"; const BASE_URL = process.env.API_ROOT || `http://localhost:${port}`; @@ -13,7 +14,11 @@ export default async function ApiProxy(req, res) { var containerName = req.query.container; if (!hasValue(containerName)) { - return res.status(400).json(); + return respondError(res, { + status: 400, + code: "MISSING_REQUIRED_QUERY", + message: "container is required" + }); } var token = await getToken(); @@ -26,10 +31,14 @@ export default async function ApiProxy(req, res) { azureHeaders(token.access_token) ) .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: "GET_REPS_BLOB_PROXY_FAILED", + message: "Failed to fetch representation blobs" + }); }); } From e68319f16cdf605dda5e6f94af0a13061a9c9972 Mon Sep 17 00:00:00 2001 From: robbond Date: Tue, 17 Mar 2026 13:06:28 +0000 Subject: [PATCH 4/7] TASK22102: slice4 larger file set + production detail suppression --- memory-bank/change-log.md | 34 +++++++++++++++++++ .../file/createappealcompletemessage_api.js | 21 +++++++++--- pages/api/file/createcase_api.js | 16 +++++++-- pages/api/file/createcaseinvolvement_api.js | 17 +++++++--- pages/api/file/createrepinvolvement_api.js | 17 +++++++--- pages/api/file/generatepdf.js | 21 +++++++++--- pages/api/file/updatecase_api.js | 16 +++++++-- pages/api/middleware/apiResponse.js | 3 +- 8 files changed, 122 insertions(+), 23 deletions(-) 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; } From 3048df3cd93c3d737bc7f857bf6d553a051369c8 Mon Sep 17 00:00:00 2001 From: robbond Date: Tue, 17 Mar 2026 13:16:48 +0000 Subject: [PATCH 5/7] TASK22102: slice5 larger file handler contract cleanup --- memory-bank/change-log.md | 35 +++++++++++++++++++ .../api/file/createrepcompletemessage_api.js | 21 ++++++++--- pages/api/file/editRepJson.js | 28 +++++++++++---- pages/api/file/setupcontainer.js | 21 ++++++++--- pages/api/file/uploadsinglefile.js | 27 +++++++++++--- 5 files changed, 112 insertions(+), 20 deletions(-) diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index 44922c9c..79e65e3d 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -1437,3 +1437,38 @@ Validation: Follow-ups: - Continue next larger slice on remaining non-proxy file handlers still returning raw error payloads. + +--- + +### CL-039: TASK22102 API contract consistency — Slice 5 (larger file-handler batch) + +date: 2026-03-17 +author: Cline +scope: `pages/api/file/{uploadsinglefile,setupcontainer,createrepcompletemessage_api,editRepJson}.js` +type: change +rationale: Continue with a larger efficiency-focused slice by migrating remaining high-signal file handlers that still returned raw/empty error responses. +impact: Improves response-contract consistency and avoids leaking raw error payloads in selected file handlers while preserving success behavior and endpoint semantics. +status: completed + +Summary: + +- Migrated additional file handlers to helper-based responses: + - `uploadsinglefile` + - `setupcontainer` + - `createrepcompletemessage_api` + - `editRepJson` +- Replaced direct `res.status(...).json(...)` error branches (including raw `error` payload and empty responses) with structured: + - `respondError(...)` + - `respondSuccess(...)` +- Preserved existing success semantics: + - `uploadsinglefile` still returns uploaded result + `invalidFiles` + - `setupcontainer` still returns `{ data: "success", output }` + - `editRepJson` still returns success message on completion + +Validation: + +- `npx next lint --file pages/api/file/uploadsinglefile.js --file pages/api/file/setupcontainer.js --file pages/api/file/createrepcompletemessage_api.js --file pages/api/file/editRepJson.js` -> pass (no warnings/errors) + +Follow-ups: + +- Continue next larger slice on remaining file handlers with empty 400 responses to complete contract-consistency rollout. diff --git a/pages/api/file/createrepcompletemessage_api.js b/pages/api/file/createrepcompletemessage_api.js index 1f30911a..d682d651 100644 --- a/pages/api/file/createrepcompletemessage_api.js +++ b/pages/api/file/createrepcompletemessage_api.js @@ -4,6 +4,7 @@ import { } from "../../../actions/azurestorage"; import { hashAPIPath } from "../../../actions/core/hash"; import { consoleLogger } from "../../../actions/core/logger"; +import { respondError, respondSuccess } from "../middleware/apiResponse"; import nextConnect from "next-connect"; import middleware from "../middleware/middleware"; @@ -27,7 +28,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, repid and hash are required" + }); } var checkquerypath = @@ -39,16 +44,24 @@ ApiProxy.get(async (req, res) => { filename; if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { - return res.status(400).json(); + return respondError(res, { + status: 400, + code: "INVALID_HASH", + message: "Invalid hash" + }); } await createRepCompleteMessage(containerName, tempCaseRef, filename) .then((data) => { - return res.status(200).json(data); + return respondSuccess(res, data); }) .catch((error) => { consoleLogger(error); - return res.status(400).json(); + return respondError(res, { + status: 400, + code: "CREATE_REP_COMPLETE_MESSAGE_FAILED", + message: "Failed to create representation complete message" + }); }); }); diff --git a/pages/api/file/editRepJson.js b/pages/api/file/editRepJson.js index 20a19c4f..e301ade4 100644 --- a/pages/api/file/editRepJson.js +++ b/pages/api/file/editRepJson.js @@ -1,17 +1,25 @@ import { DefaultAzureCredential } from "@azure/identity"; import { BlobServiceClient } from "@azure/storage-blob"; +import { consoleLogger } from "../../../actions/core/logger"; +import { respondError, respondSuccess } from "../middleware/apiResponse"; export default async function handler(req, res) { if (req.method !== "POST") { - res.status(405).json({ error: "Method not allowed" }); - return; + return respondError(res, { + status: 405, + code: "METHOD_NOT_ALLOWED", + message: "Method not allowed" + }); } const { container, blobName } = req.body; if (!container || !blobName) { - res.status(400).json({ error: "Missing container or blobName" }); - return; + return respondError(res, { + status: 400, + code: "MISSING_REQUIRED_BODY", + message: "Missing container or blobName" + }); } try { @@ -60,10 +68,16 @@ export default async function handler(req, res) { const withTags = await blockBlobClient.setTags(tags); const withMeta = await blockBlobClient.setMetadata(tags); - res.status(200).json({ message: "repComplete removed successfully" }); + return respondSuccess(res, { + message: "repComplete removed successfully" + }); } catch (error) { - console.error("Error editing rep.json:", error); - res.status(500).json({ error: "Failed to edit rep.json" }); + consoleLogger(error); + return respondError(res, { + status: 500, + code: "EDIT_REP_JSON_FAILED", + message: "Failed to edit rep.json" + }); } } diff --git a/pages/api/file/setupcontainer.js b/pages/api/file/setupcontainer.js index f02ba816..ca2c5d00 100644 --- a/pages/api/file/setupcontainer.js +++ b/pages/api/file/setupcontainer.js @@ -7,6 +7,7 @@ import { } from "../../../actions/azurestorage"; import { hashAPIPath } from "../../../actions/core/hash"; import { consoleLogger } from "../../../actions/core/logger"; +import { respondError, respondSuccess } from "../middleware/apiResponse"; import middleware from "../middleware/middleware"; import nextConnect from "next-connect"; @@ -24,22 +25,34 @@ 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: "ident and hash are required" + }); } var checkquerypath = "/api/file/setupcontainer?ident=" + containerName; if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { - return res.status(400).json(); + return respondError(res, { + status: 400, + code: "INVALID_HASH", + message: "Invalid hash" + }); } await createContainer(containerName) .then((data) => { - return res.status(200).json({ data: "success", output: data }); + return respondSuccess(res, { data: "success", output: data }); }) .catch((error) => { consoleLogger(error); - res.status(400).json(error); + return respondError(res, { + status: 400, + code: "SETUP_CONTAINER_FAILED", + message: "Failed to setup container" + }); }); }); diff --git a/pages/api/file/uploadsinglefile.js b/pages/api/file/uploadsinglefile.js index ffbf222e..d5c0a99c 100644 --- a/pages/api/file/uploadsinglefile.js +++ b/pages/api/file/uploadsinglefile.js @@ -8,6 +8,7 @@ import nextConnect from "next-connect"; import middleware from "../middleware/middleware"; import { consoleLogger } from "../../../actions/core/logger"; import { hashAPIPath } from "../../../actions/core/hash"; +import { respondError, respondSuccess } from "../middleware/apiResponse"; import { fileTypeFromBuffer } from "file-type"; import fs from "fs"; import path from "path"; @@ -39,13 +40,21 @@ ApiProxy.post(async (req, res) => { var checkHash = req.query.hash; if (typeof checkHash === "undefined" || checkHash.length === 0) { - return res.status(400).json(); + return respondError(res, { + status: 400, + code: "HASH_REQUIRED", + message: "hash is required" + }); } var checkquerypath = "/api/file/uploadsinglefile"; if (hashAPIPath(checkquerypath) != "?hash=" + checkHash) { - return res.status(400).json(); + return respondError(res, { + status: 400, + code: "INVALID_HASH", + message: "Invalid hash" + }); } const containerID = req.body?.containerID?.[0]; @@ -57,7 +66,11 @@ ApiProxy.post(async (req, res) => { typeof casefolderID === "undefined" || casefolderID.length === 0 ) { - return res.status(400).json(); + return respondError(res, { + status: 400, + code: "MISSING_REQUIRED_BODY", + message: "containerID and casefolderID are required" + }); } const uploadedFiles = req.files || {}; @@ -126,10 +139,14 @@ ApiProxy.post(async (req, res) => { containerID, casefolderID ); - return res.status(200).json({ data, invalidFiles }); + return respondSuccess(res, { data, invalidFiles }); } catch (error) { consoleLogger(error); - return res.status(500).json({ error: "Failed to upload files" }); + return respondError(res, { + status: 500, + code: "UPLOAD_SINGLE_FILE_FAILED", + message: "Failed to upload files" + }); } }); From 98184b65df794c9e5164cd95de3f97d77ea9a0d6 Mon Sep 17 00:00:00 2001 From: robbond Date: Tue, 17 Mar 2026 13:23:01 +0000 Subject: [PATCH 6/7] TASK22102: slice6 blob crud and retrieval contract cleanup --- memory-bank/change-log.md | 36 +++++++++++++++ .../file/deleteawaitingsubmissionfromblob.js | 15 +++++-- pages/api/file/deleteblob.js | 15 +++++-- pages/api/file/deleteblobcase.js | 21 ++++++--- pages/api/file/deleteblobrep.js | 21 ++++++--- .../api/file/getawaitingsubmissionfromblob.js | 29 +++++++----- pages/api/file/getprogressobjblob.js | 15 +++++-- pages/api/file/getrepsblob.js | 45 +++++++++++-------- 8 files changed, 145 insertions(+), 52 deletions(-) diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index 79e65e3d..7b86df07 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -1472,3 +1472,39 @@ Validation: Follow-ups: - Continue next larger slice on remaining file handlers with empty 400 responses to complete contract-consistency rollout. + +--- + +### CL-040: TASK22102 API contract consistency — Slice 6 (CRUD/blob retrieval group) + +date: 2026-03-17 +author: Cline +scope: `pages/api/file/{deleteblob,deleteblobcase,deleteblobrep,deleteawaitingsubmissionfromblob,getprogressobjblob,getawaitingsubmissionfromblob,getrepsblob}.js` +type: change +rationale: Deliver the first of two remaining larger slices by standardizing response contracts across blob CRUD and retrieval handlers still using direct/empty response patterns. +impact: Improves consistency and safety of error responses in selected blob handlers while preserving existing success payload behavior and cache-control semantics. +status: completed + +Summary: + +- Migrated 7 blob CRUD/retrieval handlers to `respondSuccess`/`respondError`: + - `deleteblob` + - `deleteblobcase` + - `deleteblobrep` + - `deleteawaitingsubmissionfromblob` + - `getprogressobjblob` + - `getawaitingsubmissionfromblob` + - `getrepsblob` +- Replaced empty `400` and direct status responses with structured error envelopes including explicit codes/messages. +- Removed redundant post-hash equality branches where equivalent logic was already guaranteed after early guard checks. +- Preserved response behavior: + - existing success data payload shapes retained (`{ data: ... }` or direct data payloads) + - `getrepsblob` retains `Cache-Control: no-store` header. + +Validation: + +- `npx next lint --file pages/api/file/deleteblob.js --file pages/api/file/deleteblobcase.js --file pages/api/file/deleteblobrep.js --file pages/api/file/deleteawaitingsubmissionfromblob.js --file pages/api/file/getprogressobjblob.js --file pages/api/file/getawaitingsubmissionfromblob.js --file pages/api/file/getrepsblob.js` -> pass (no warnings/errors) + +Follow-ups: + +- Execute final Slice 7 for remaining complex handlers (`upload`, `getbloblist`, `downloadblob`, `generateappealpdf`) to complete the 2-slice finish plan. diff --git a/pages/api/file/deleteawaitingsubmissionfromblob.js b/pages/api/file/deleteawaitingsubmissionfromblob.js index cc99f15e..a1cdbe53 100644 --- a/pages/api/file/deleteawaitingsubmissionfromblob.js +++ b/pages/api/file/deleteawaitingsubmissionfromblob.js @@ -1,5 +1,6 @@ import { hashAPIPath } from "../../../actions/core/hash"; import { deleteBlob } from "../../../actions/azurestorage"; +import { respondError, respondSuccess } from "../middleware/apiResponse"; import nextConnect from "next-connect"; import middleware from "../middleware/middleware"; @@ -23,7 +24,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, casefolderID, blobname and hash are required" + }); } var checkquerypath = @@ -35,12 +40,16 @@ ApiProxy.get(async (req, res) => { blobName; if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { - return res.status(400).json(); + return respondError(res, { + status: 400, + code: "INVALID_HASH", + message: "Invalid hash" + }); } await deleteBlob(containerName, casefolderID + "/files/" + blobName).then( (data) => { - return res.status(200).json({ data: data }); + return respondSuccess(res, { data: data }); } ); }); diff --git a/pages/api/file/deleteblob.js b/pages/api/file/deleteblob.js index 7ff99c4f..5c1d52fe 100644 --- a/pages/api/file/deleteblob.js +++ b/pages/api/file/deleteblob.js @@ -1,5 +1,6 @@ import { hashAPIPath } from "../../../actions/core/hash"; import { deleteBlob } from "../../../actions/azurestorage"; +import { respondError, respondSuccess } from "../middleware/apiResponse"; import nextConnect from "next-connect"; import middleware from "../middleware/middleware"; @@ -23,7 +24,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, casefolderID, blobname and hash are required" + }); } var checkquerypath = @@ -35,12 +40,16 @@ ApiProxy.get(async (req, res) => { encodeURIComponent(blobName); if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { - return res.status(400).json(); + return respondError(res, { + status: 400, + code: "INVALID_HASH", + message: "Invalid hash" + }); } await deleteBlob(containerName, casefolderID + "/files/" + blobName).then( (data) => { - return res.status(200).json({ data: data }); + return respondSuccess(res, { data: data }); } ); }); diff --git a/pages/api/file/deleteblobcase.js b/pages/api/file/deleteblobcase.js index 39e2cfde..ba48e9d5 100644 --- a/pages/api/file/deleteblobcase.js +++ b/pages/api/file/deleteblobcase.js @@ -1,5 +1,6 @@ import { hashAPIPath } from "../../../actions/core/hash"; import { deleteBlobCase } from "../../../actions/azurestorage"; +import { respondError, respondSuccess } from "../middleware/apiResponse"; import nextConnect from "next-connect"; import middleware from "../middleware/middleware"; @@ -20,7 +21,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, casefolderID and hash are required" + }); } var checkquerypath = @@ -30,14 +35,16 @@ ApiProxy.get(async (req, res) => { casefolderID; if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { - return res.status(400).json(); - } - - if (hashAPIPath(checkquerypath) == "&hash=" + checkHash) { - await deleteBlobCase(containerName, casefolderID).then((data) => { - return res.status(200).json({ data: data }); + return respondError(res, { + status: 400, + code: "INVALID_HASH", + message: "Invalid hash" }); } + + await deleteBlobCase(containerName, casefolderID).then((data) => { + return respondSuccess(res, { data: data }); + }); }); export const config = { diff --git a/pages/api/file/deleteblobrep.js b/pages/api/file/deleteblobrep.js index 5d55a259..fd80f28b 100644 --- a/pages/api/file/deleteblobrep.js +++ b/pages/api/file/deleteblobrep.js @@ -1,5 +1,6 @@ import { hashAPIPath } from "../../../actions/core/hash"; import { deleteBlobRep } from "../../../actions/azurestorage"; +import { respondError, respondSuccess } from "../middleware/apiResponse"; import nextConnect from "next-connect"; import middleware from "../middleware/middleware"; @@ -23,7 +24,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, casefolderID, repfile and hash are required" + }); } var checkquerypath = @@ -35,16 +40,18 @@ ApiProxy.get(async (req, res) => { repfile; if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { - return res.status(400).json(); + return respondError(res, { + status: 400, + code: "INVALID_HASH", + message: "Invalid hash" + }); } casefolderID = casefolderID + "/" + repfile; - if (hashAPIPath(checkquerypath) == "&hash=" + checkHash) { - await deleteBlobRep(containerName, casefolderID).then((data) => { - return res.status(200).json({ data: data }); - }); - } + await deleteBlobRep(containerName, casefolderID).then((data) => { + return respondSuccess(res, { data: data }); + }); }); export const config = { diff --git a/pages/api/file/getawaitingsubmissionfromblob.js b/pages/api/file/getawaitingsubmissionfromblob.js index c1949040..d8ca4315 100644 --- a/pages/api/file/getawaitingsubmissionfromblob.js +++ b/pages/api/file/getawaitingsubmissionfromblob.js @@ -6,6 +6,7 @@ import _ from "lodash"; import nextConnect from "next-connect"; import middleware from "../middleware/middleware"; import { hashAPIPath } from "../../../actions/core/hash"; +import { respondError, respondSuccess } from "../middleware/apiResponse"; const ApiProxy = nextConnect(); ApiProxy.use(middleware); @@ -20,25 +21,31 @@ 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 and hash are required" + }); } var checkquerypath = "/api/file/getawaitingsubmissionfromblob?container=" + containerName; if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { - return res.status(400).json(); + return respondError(res, { + status: 400, + code: "INVALID_HASH", + message: "Invalid hash" + }); } - if (hashAPIPath(checkquerypath) == "&hash=" + checkHash) { - const blobObj = await getAllProgressBlobs(containerName) - .then((data) => { - return downloadAllProgressFiles(containerName, data); - }) - .then((data) => { - return res.status(200).json(data); - }); - } + const blobObj = await getAllProgressBlobs(containerName) + .then((data) => { + return downloadAllProgressFiles(containerName, data); + }) + .then((data) => { + return respondSuccess(res, data); + }); }); export const config = { diff --git a/pages/api/file/getprogressobjblob.js b/pages/api/file/getprogressobjblob.js index 6df0a3ac..a9481aa4 100644 --- a/pages/api/file/getprogressobjblob.js +++ b/pages/api/file/getprogressobjblob.js @@ -6,6 +6,7 @@ import _ from "lodash"; import nextConnect from "next-connect"; import middleware from "../middleware/middleware"; import { hashAPIPath } from "../../../actions/core/hash"; +import { respondError, respondSuccess } from "../middleware/apiResponse"; const ApiProxy = nextConnect(); ApiProxy.use(middleware); @@ -24,7 +25,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, casefolderID and hash are required" + }); } var checkquerypath = @@ -34,7 +39,11 @@ ApiProxy.get(async (req, res) => { casefolderID; if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { - return res.status(400).json(); + return respondError(res, { + status: 400, + code: "INVALID_HASH", + message: "Invalid hash" + }); } await getProgressBlobs(containerName, casefolderID) @@ -42,7 +51,7 @@ ApiProxy.get(async (req, res) => { return downloadProgressFile(containerName, data.path, casefolderID); }) .then((data) => { - return res.status(200).json(data); + return respondSuccess(res, data); }); }); diff --git a/pages/api/file/getrepsblob.js b/pages/api/file/getrepsblob.js index a7177949..a059f5a2 100644 --- a/pages/api/file/getrepsblob.js +++ b/pages/api/file/getrepsblob.js @@ -38,6 +38,7 @@ import nextConnect from "next-connect"; import middleware from "../middleware/middleware"; import { hashAPIPath } from "../../../actions/core/hash"; import { consoleLogger } from "../../../actions/core/logger"; +import { respondError, respondSuccess } from "../middleware/apiResponse"; const ApiProxy = nextConnect(); ApiProxy.use(middleware); @@ -54,32 +55,40 @@ 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 and hash are required" + }); } var checkquerypath = "/api/file/getrepsblob?container=" + containerName; if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { - return res.status(400).json(); + return respondError(res, { + status: 400, + code: "INVALID_HASH", + message: "Invalid hash" + }); } - if (hashAPIPath(checkquerypath) == "&hash=" + checkHash) { - const blobObj = await getRepsBlobs(containerName) - .then(async (data) => { - return data; - }) - .then(async (data) => { - let result = await downloadAllRepsFiles(containerName, data); - - return res - .setHeader("Cache-Control", "no-store") - .status(200) - .json(result); - }) - .catch((error) => { - consoleLogger(error); + const blobObj = await getRepsBlobs(containerName) + .then(async (data) => { + return data; + }) + .then(async (data) => { + let result = await downloadAllRepsFiles(containerName, data); + res.setHeader("Cache-Control", "no-store"); + return respondSuccess(res, result); + }) + .catch((error) => { + consoleLogger(error); + return respondError(res, { + status: 400, + code: "GET_REPS_BLOB_FAILED", + message: "Failed to retrieve representation blobs" }); - } + }); }); export default ApiProxy; From 91890f4d738d205a820760b633bb4e070120f6e7 Mon Sep 17 00:00:00 2001 From: robbond Date: Tue, 17 Mar 2026 13:25:51 +0000 Subject: [PATCH 7/7] TASK22102: slice7 complex file handler contract closeout --- pages/api/file/downloadblob.js | 37 ++++++++++++-------- pages/api/file/generateappealpdf.js | 52 +++++++++++++++++++--------- pages/api/file/getbloblist.js | 34 +++++++++++------- pages/api/file/upload.js | 53 +++++++++++++++++------------ 4 files changed, 109 insertions(+), 67 deletions(-) diff --git a/pages/api/file/downloadblob.js b/pages/api/file/downloadblob.js index eabd0aa8..f7683731 100644 --- a/pages/api/file/downloadblob.js +++ b/pages/api/file/downloadblob.js @@ -3,6 +3,7 @@ import { downloadFile } from "../../../actions/azurestorage"; import nextConnect from "next-connect"; import { hashAPIPath } from "../../../actions/core/hash"; import middleware from "../middleware/middleware"; +import { respondError } from "../middleware/apiResponse"; const ApiProxy = nextConnect(); @@ -24,7 +25,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, casefolderID, blobname and hash are required" + }); } var checkquerypath = @@ -36,24 +41,26 @@ ApiProxy.get(async (req, res) => { blobName.trim(); if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { - return res.status(400).json(); + return respondError(res, { + status: 400, + code: "INVALID_HASH", + message: "Invalid hash" + }); } - if (hashAPIPath(checkquerypath) == "&hash=" + checkHash) { - const bloblocation = - casefolderID + (blobName.indexOf(".json") > 0 ? "/" : "/files/"); + const bloblocation = + casefolderID + (blobName.indexOf(".json") > 0 ? "/" : "/files/"); - const downloaded = await downloadFile( - containerName, - bloblocation + decodeURI(blobName) - ); + const downloaded = await downloadFile( + containerName, + bloblocation + decodeURI(blobName) + ); - res.setHeader( - "content-disposition", - "attachment; filename=" + decodeURI(blobName) - ); - return res.status(200).send(downloaded); - } + res.setHeader( + "content-disposition", + "attachment; filename=" + decodeURI(blobName) + ); + return res.status(200).send(downloaded); }); async function streamToBuffer(readableStream) { diff --git a/pages/api/file/generateappealpdf.js b/pages/api/file/generateappealpdf.js index 5915501a..e90284c9 100644 --- a/pages/api/file/generateappealpdf.js +++ b/pages/api/file/generateappealpdf.js @@ -104,6 +104,7 @@ import { finalComments_pdf } from "../../../components/pdftemplates/finalComment 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 { respondError, respondSuccess } from "../middleware/apiResponse"; //const ApiProxy = nextConnect(); // ApiProxy.use(middleware); @@ -132,7 +133,12 @@ export default async function handler(req, res) { typeof casefolderID === "undefined" || String(casefolderID).length === 0 ) { - return res.status(400).json(); + return respondError(res, { + status: 400, + code: "MISSING_REQUIRED_QUERY", + message: + "hash, appealType, containerID and casefolderID are required" + }); } checkquerypath = @@ -142,7 +148,11 @@ export default async function handler(req, res) { (isDownload ? "&download=true" : ""); if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { - return res.status(400).json(); + return respondError(res, { + status: 400, + code: "INVALID_HASH", + message: "Invalid hash" + }); } // Create Document Component @@ -238,20 +248,28 @@ export default async function handler(req, res) { renderedPDFBuffer, containerID, blobProgress.pinswg_name || blobProgress.caseObj.ticketnumber - ).then((data) => { - if (isDownload) { - const filename = `${pdfFileName}.pdf`; - res.setHeader("Content-Type", "application/pdf"); - res.setHeader( - "Content-Disposition", - `attachment; filename="${filename}"` - ); - return res.status(200).send(renderedPDFBuffer); - } - return res.status(200).json({ - status: "success", - data: data, - path: `/files/${pdfFileName}.pdf` + ) + .then((data) => { + if (isDownload) { + const filename = `${pdfFileName}.pdf`; + res.setHeader("Content-Type", "application/pdf"); + res.setHeader( + "Content-Disposition", + `attachment; filename="${filename}"` + ); + return res.status(200).send(renderedPDFBuffer); + } + return respondSuccess(res, { + status: "success", + data: data, + path: `/files/${pdfFileName}.pdf` + }); + }) + .catch(() => { + return respondError(res, { + status: 400, + code: "GENERATE_APPEAL_PDF_FAILED", + message: "Failed to generate appeal PDF" + }); }); - }); } diff --git a/pages/api/file/getbloblist.js b/pages/api/file/getbloblist.js index ff56f48c..da29a655 100644 --- a/pages/api/file/getbloblist.js +++ b/pages/api/file/getbloblist.js @@ -31,6 +31,7 @@ import { hashAPIPath } from "../../../actions/core/hash"; import { getBlobs, getRepsFilesBlobs } from "../../../actions/azurestorage"; +import { respondError, respondSuccess } from "../middleware/apiResponse"; import nextConnect from "next-connect"; import middleware from "../middleware/middleware"; @@ -52,7 +53,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, casefolderID and hash are required" + }); } var checkquerypath = @@ -62,20 +67,23 @@ ApiProxy.get(async (req, res) => { casefolderID; if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { - return res.status(400).json(); + return respondError(res, { + status: 400, + code: "INVALID_HASH", + message: "Invalid hash" + }); } - casefolderID.split("/").length > 1 - ? await getRepsFilesBlobs( - containerName, - casefolderID.split("/")[0], - casefolderID.split("/")[1] - ).then((data) => { - return res.status(200).json({ "value": [data] }); - }) - : await getBlobs(containerName, casefolderID).then((data) => { - return res.status(200).json({ "value": [data] }); - }); + const data = + casefolderID.split("/").length > 1 + ? await getRepsFilesBlobs( + containerName, + casefolderID.split("/")[0], + casefolderID.split("/")[1] + ) + : await getBlobs(containerName, casefolderID); + + return respondSuccess(res, { "value": [data] }); }); export const config = { diff --git a/pages/api/file/upload.js b/pages/api/file/upload.js index 1185f174..80038527 100644 --- a/pages/api/file/upload.js +++ b/pages/api/file/upload.js @@ -4,6 +4,7 @@ import { uploadFile } from "../../../actions/azurestorage"; import { hashAPIPath } from "../../../actions/core/hash"; +import { respondError, respondSuccess } from "../middleware/apiResponse"; import nextConnect from "next-connect"; import middleware from "../middleware/middleware"; @@ -15,13 +16,21 @@ ApiProxy.post(async (req, res) => { var checkHash = req.query.hash; if (typeof checkHash === "undefined" || checkHash.length === 0) { - return res.status(400).json(); + return respondError(res, { + status: 400, + code: "HASH_REQUIRED", + message: "hash is required" + }); } var checkquerypath = "/api/file/upload"; if (hashAPIPath(checkquerypath) != "?hash=" + checkHash) { - return res.status(400).json(); + return respondError(res, { + status: 400, + code: "INVALID_HASH", + message: "Invalid hash" + }); } const appealData = req.body.appealData; @@ -35,28 +44,28 @@ ApiProxy.post(async (req, res) => { typeof casefolderID === "undefined" || casefolderID.length === 0 ) { - return res.status(400).json(); + return respondError(res, { + status: 400, + code: "MISSING_REQUIRED_BODY", + message: "containerID and casefolderID are required" + }); } - repOrAppeal - ? createRepBlob(appealData, containerID, casefolderID).then((data) => { - // Object.keys(req.files).length > 0 && - // uploadFile(req.files, containerID, casefolderID).then( - // (data) => { - // return res.status(200).json({ data }); - // } - // ); - return res.status(200).json({ data }); - }) - : createBlob(appealData, containerID, casefolderID).then((data) => { - // Object.keys(req.files).length > 0 && - // uploadFile(req.files, containerID, casefolderID).then( - // (data) => { - // return res.status(200).json({ data }); - // } - // ); - return res.status(200).json({ data }); - }); + return ( + repOrAppeal + ? createRepBlob(appealData, containerID, casefolderID) + : createBlob(appealData, containerID, casefolderID) + ) + .then((data) => { + return respondSuccess(res, { data }); + }) + .catch(() => { + return respondError(res, { + status: 400, + code: "UPLOAD_FAILED", + message: "Failed to upload blob" + }); + }); }); export const config = {