Merged PR 2201: update file endpoints

Related work items: #22224
This commit is contained in:
Robert Bond
2026-03-24 06:30:29 +00:00
44 changed files with 3162 additions and 1093 deletions
+18 -5
View File
@@ -1384,14 +1384,27 @@ export const getRepsBlobs = async (containerName) => {
listOptions
)) {
const blobClient = containerClient.getBlobClient(blob.name);
//console.log("getreps blob:", blob);
blob.name.split("/")[2].indexOf("_rep.json") > 0 &&
blob.name.split("/")[2].indexOf("undefined") < 0 &&
// Filter out soft-deleted/stale tag entries and malformed names.
const namePart = blob.name.split("/")[2] ?? "";
if (
namePart.indexOf("_rep.json") <= 0 ||
namePart.indexOf("undefined") >= 0
)
continue;
try {
const properties = await blobClient.getProperties();
blobObj.push({
"name": blob.name.split("/")[2],
"name": namePart,
"path": blob.name,
"size": blobClient.getProperties().contentLength
"size": properties.contentLength
});
} catch (error) {
if (error?.statusCode === 404) continue;
throw error;
}
}
//console.log("blobObjwwwww:", blobObj);
+2 -5
View File
@@ -130,7 +130,6 @@ const MyPortal = (props) => {
{!isLPA && (
<MakeNewAppeal docsOffline={docsOffline} />
)}
{/* {props.awaitingSubmission.awaitingSubmission[
"@odata.count"
] > 0 && (
@@ -147,9 +146,8 @@ const MyPortal = (props) => {
"@odata.count"
]) &&
props.awaitingSubmission
.awaitingSubmissionFromBlob[
"@odata.count"
] > 0 && (
.awaitingSubmissionFromBlob.case.length >
0 && (
<AwaitingSubmissionFromBlob
containerID={props.containerID}
awaitingSubmissionFromBlob={
@@ -189,7 +187,6 @@ const MyPortal = (props) => {
isLPA={isLPA}
/>
)}
{props.myRepresentations.mySubmittedReps
.mySubmittedReps.length > 0 && (
<MySubmittedReps
+596
View File
@@ -129,3 +129,599 @@ Follow-ups:
- Continue with remaining non-standard API outlier(s), notably `pages/api/file/generateappealpdfcopy.js`.
- Keep future slices logged in this file at commit-time now that memory-bank is versioned.
---
### CL-004: TASK22224 file + static endpoint contract hardening bundle (phase21)
date: 2026-03-23
author: Cline
scope: `pages/api/file/{downloadblob,generateappealpdfcopy}.js`, `pages/api/endpoint/{getsipsmedia_api,getappealtypesfornewappeal_api}.js`, `tests/phase21/{file-handler-contract,endpoint-handler-contract}.test.cjs`
type: change
rationale: Deliver the agreed larger bounded slice for remaining non-standard file/static handlers, improving negative-path consistency while preserving current success payload behavior.
impact: Standardized error envelopes/codes for download and generated PDF copy flows, method guard parity for static endpoints, and expanded phase21 contract coverage for both file and endpoint handlers.
status: completed
Summary:
- `downloadblob.js`:
- added explicit catch-path response via `respondError` with `DOWNLOAD_BLOB_FAILED`
- kept success behavior intact (attachment header + raw file body)
- removed dead internal helper (`streamToBuffer`) and tightened local declarations
- `generateappealpdfcopy.js`:
- removed unused imports/noisy console warnings
- standardized required-input and negative-path contracts:
- `INCIDENT_ID_REQUIRED` (400)
- `CASE_NOT_FOUND` (404)
- `FORM_COLLECTION_NOT_FOUND` (400)
- `APPEAL_PDF_COPY_GENERATION_FAILED` (400)
- preserved success output contract (PDF content headers + buffer body)
- `getsipsmedia_api.js` and `getappealtypesfornewappeal_api.js`:
- added method guard for non-GET requests using `METHOD_NOT_ALLOWED` (405)
- preserved existing GET success payloads
- Expanded phase21 tests:
- `file-handler-contract.test.cjs`: added coverage for download failure + full generated PDF copy contract/negative paths
- `endpoint-handler-contract.test.cjs`: added method guard tests for both static endpoints
Validation:
- `node tests/phase21/api-contract-slice1.test.cjs` -> pass
- helper: 4/4
- file-handler: 17/17
- email-handler: 12/12
- endpoint-handler: 149/149
- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps`; no new lint errors)
Follow-ups:
- If desired, next slice can target remaining file-route parity candidates outside this bundle, but this closes the planned TASK22224 scope.
---
### CL-005: TASK22224 downloadblob hotfix closure (path normalization + hash compatibility)
date: 2026-03-23
author: Cline
scope: `pages/api/file/downloadblob.js`
type: change
rationale: Close post-merge runtime regressions reported on live links where download URLs alternated between filename-only/full-path blob names and mixed encoded/raw hash input variants.
impact: Restored reliable blob downloads without relaxing hash security guarantees (still HMAC validated), and preserved existing caller compatibility across legacy/new URL encodings.
status: completed
Summary:
- Hotfix 1 (`f09f3b7`): normalized blob path resolution
- accepts both forms of `blobname` input:
- filename only (legacy)
- full prefixed path (already includes `casefolderID/...`)
- prevents double-prefix lookup failures
- sets attachment filename from final path segment only
- Hotfix 2 (`bd3bf68`): hash compatibility validation
- validates against a bounded set of canonical query-path variants (raw/encoded combinations for `casefolderID` and `blobname`)
- fixes `INVALID_HASH` false negatives for legitimate caller-generated links
- keeps strict HMAC requirement in place (no unauthenticated bypass)
Validation:
- `node tests/phase21/file-handler-contract.test.cjs` -> pass (17/17)
- `node tests/phase21/api-contract-slice1.test.cjs` -> pass
- helper: 4/4
- file-handler: 17/17
- email-handler: 12/12
- endpoint-handler: 149/149
- User confirmation: "downloadblob now works"
Follow-ups:
- Next recommended slice on this branch: complete file-route guard parity for `deleteblob.js`, `deleteblobcase.js`, and `deleteblobrep.js` by aligning hash validation canonicalization and explicit `respondError` contracts (`MISSING_REQUIRED_QUERY`, `INVALID_HASH`, operation-specific `*_FAILED`).
- Extend `tests/phase21/file-handler-contract.test.cjs` for the above routes with mixed encoded/raw hash cases to lock compatibility.
---
### CL-006: TASK22224 file delete-route guard parity slice
date: 2026-03-23
author: Cline
scope: `pages/api/file/{deleteblob,deleteblobcase,deleteblobrep}.js`, `tests/phase21/file-handler-contract.test.cjs`
type: change
rationale: Execute the next planned slice to align hash/canonicalization behavior and negative-path contracts across high-risk file delete routes, matching the compatibility posture established for `downloadblob`.
impact: Reduces false `INVALID_HASH` failures for legitimate encoded/raw caller variants while preserving strict hash enforcement and improving resilience via explicit catch-path contracts.
status: completed
Summary:
- `deleteblob.js`
- added bounded hash candidate validation for encoded/raw combinations of `casefolderID` and `blobname`
- normalized delete path handling for both filename-only and already-prefixed blob paths
- added explicit catch-path contract: `DELETE_BLOB_FAILED`
- `deleteblobcase.js`
- added hash candidate validation for raw/encoded `casefolderID`
- added explicit catch-path contract: `DELETE_BLOB_CASE_FAILED`
- `deleteblobrep.js`
- added hash candidate validation for encoded/raw `casefolderID` + `repfile`
- added explicit catch-path contract: `DELETE_BLOB_REP_FAILED`
- Phase21 tests expanded (`file-handler-contract.test.cjs`):
- encoded hash-variant acceptance cases for all three delete routes
- explicit dependency-failure contract assertions for all three delete routes
Validation:
- `node tests/phase21/file-handler-contract.test.cjs` -> pass (23/23)
- `node tests/phase21/api-contract-slice1.test.cjs` -> pass
- helper: 4/4
- file-handler: 23/23
- email-handler: 12/12
- endpoint-handler: 149/149
Follow-ups:
- Optional next slice: apply same bounded hash-canonicalization parity to remaining high-sensitivity file routes where mixed encoded/raw callers may exist (`getbloblist`, `getprogressobjblob`) and add regression cases to phase21.
---
### CL-007: TASK22224 getrepsblob stability hotfix after delete representation flow
date: 2026-03-23
author: Cline
scope: `actions/azurestorage.js` (`getRepsBlobs`), `tests/phase21/file-handler-contract.test.cjs`
type: change
rationale: Resolve reported runtime 400 (`GET_REPS_BLOB_FAILED`) after delete representation actions, caused by stale soft-deleted blob tag hits during representation blob enumeration.
impact: Prevents transient/stale Azure tag index entries from breaking representation retrieval, improving reliability of post-delete refresh without relaxing route security contracts.
status: completed
Summary:
- Hardened `getRepsBlobs(containerName)` in `actions/azurestorage.js`:
- fixed async misuse (`blobClient.getProperties().contentLength` without await)
- added existence/property guard with explicit `await blobClient.getProperties()`
- skips 404s (soft-deleted/stale tag index results) instead of throwing
- preserves behavior for non-404 failures (rethrow for proper error visibility)
- kept existing `_rep.json`/`undefined` name filtering intact
- Added phase21 contract coverage for `getrepsblob` route:
- success payload contract test
- dependency failure contract test (`GET_REPS_BLOB_FAILED`)
Validation:
- `node tests/phase21/file-handler-contract.test.cjs` -> pass (25/25)
- `node tests/phase21/api-contract-slice1.test.cjs` -> pass
- helper: 4/4
- file-handler: 25/25
- email-handler: 12/12
- endpoint-handler: 149/149
Follow-ups:
- Optional: add the same stale-tag existence guard pattern to any remaining Azure tag-list readers that still consume `findBlobsByTags` results without property existence verification.
---
### CL-008: TASK22224 awaiting-submission route resilience parity hardening
date: 2026-03-23
author: Cline
scope: `pages/api/file/getawaitingsubmissionfromblob.js`, `tests/phase21/file-handler-contract.test.cjs`
type: change
rationale: Add explicit catch-path contract parity for awaiting-submission blob retrieval route so unexpected dependency failures return consistent, actionable error envelopes.
impact: Improves reliability/diagnostics for post-delete case refresh and aligns file-route error handling style without changing success payload contract or hash verification behavior.
status: completed
Summary:
- Refactored `getawaitingsubmissionfromblob` handler to structured `try/catch` flow.
- Preserved existing guard behavior:
- `MISSING_REQUIRED_QUERY` for missing container/hash
- `INVALID_HASH` for signature mismatch
- Added explicit dependency failure contract:
- `GET_AWAITING_SUBMISSION_BLOB_FAILED` (400)
- message: `Failed to retrieve awaiting submission blobs`
- Added phase21 coverage for this route:
- success payload pass-through contract
- dependency failure contract assertion
Validation:
- `node tests/phase21/file-handler-contract.test.cjs` -> pass (27/27)
- `node tests/phase21/api-contract-slice1.test.cjs` -> pass
- helper: 4/4
- file-handler: 27/27
- email-handler: 12/12
- endpoint-handler: 149/149
Follow-ups:
- Optional parity sweep: apply the same explicit catch-path contract pattern to remaining file routes that still rely on implicit promise-chain errors.
---
### CL-009: TASK22224 proxy-route resilience and encoding parity bundle
date: 2026-03-23
author: Cline
scope: `pages/api/file/{getbloblistproxy,getrepsblobproxy,getawaitingsubmissionfromblobproxy,createappealcompletemessageproxy_api}.js`, `tests/phase21/file-handler-contract.test.cjs`
type: change
rationale: Continue larger-slice hardening by aligning proxy handlers with explicit async error handling and safer encoded upstream query forwarding for hash-based downstream calls.
impact: Improves proxy reliability and compatibility for encoded query values while preserving existing proxy error contracts and response behavior.
status: completed
Summary:
- `getbloblistproxy.js`
- converted `.then/.catch` chain to explicit `try/catch`
- encoded forwarded `container` and `casefolderID` query values
- preserved error contract: `GET_BLOB_LIST_PROXY_FAILED`
- `getrepsblobproxy.js`
- converted `.then/.catch` chain to explicit `try/catch`
- encoded forwarded `container`
- preserved error contract: `GET_REPS_BLOB_PROXY_FAILED`
- `getawaitingsubmissionfromblobproxy.js`
- converted `.then/.catch` chain to explicit `try/catch`
- preserved error contract: `GET_AWAITING_SUBMISSION_PROXY_FAILED`
- `createappealcompletemessageproxy_api.js`
- converted `.then/.catch` chain to explicit `try/catch`
- encoded forwarded `container` and `tempcaseref`
- preserved error contract: `CREATE_APPEAL_COMPLETE_MESSAGE_PROXY_FAILED`
- Phase21 tests expanded for proxy paths:
- getbloblistproxy success + dependency failure
- getrepsblobproxy success
- getawaitingsubmissionfromblobproxy dependency failure
- createappealcompletemessageproxy dependency failure
Validation:
- `node tests/phase21/file-handler-contract.test.cjs` -> pass (46/46)
- `node tests/phase21/api-contract-slice1.test.cjs` -> pass
- helper: 4/4
- file-handler: 46/46
- email-handler: 12/12
- endpoint-handler: 149/149
Follow-ups:
- Optional next big slice: bring remaining proxy/message routes using raw axios promise chains (`createcaseinvolvement_api.js`, `createrepinvolvement_api.js`, `updatecase_api.js`) onto the same async/await + explicit contract pattern.
---
### CL-010: TASK22224 involvement/update route async contract hardening bundle
date: 2026-03-23
author: Cline
scope: `pages/api/file/{createcaseinvolvement_api,createrepinvolvement_api,updatecase_api}.js`, `tests/phase21/file-handler-contract.test.cjs`
type: change
rationale: Execute the next requested slice by modernizing remaining relay-backed involvement/update handlers that still used axios promise chains and legacy dead imports.
impact: Improves reliability/readability and preserves existing response contracts, including 412 "record exists" semantics for involvement creation flows.
status: completed
Summary:
- `createcaseinvolvement_api.js`
- removed unused `CryptoJS` import
- refactored axios `.then/.catch` to explicit `try/catch`
- preserved conflict behavior: status 412 -> success `{ record: "exists" }`
- preserved failure contract: `CREATE_CASE_INVOLVEMENT_FAILED`
- `createrepinvolvement_api.js`
- removed unused `CryptoJS` import
- refactored axios `.then/.catch` to explicit `try/catch`
- preserved conflict behavior: status 412 -> success `{ record: "exists" }`
- preserved failure contract: `CREATE_REP_INVOLVEMENT_FAILED`
- `updatecase_api.js`
- removed unused `CryptoJS` import
- refactored axios `.then/.catch` to explicit `try/catch`
- preserved failure contract: `UPDATE_CASE_FAILED`
- Phase21 tests expanded:
- createcaseinvolvement 412 conflict success contract
- createrepinvolvement dependency failure contract
- updatecase dependency failure contract
Validation:
- `node tests/phase21/file-handler-contract.test.cjs` -> pass (49/49)
- `node tests/phase21/api-contract-slice1.test.cjs` -> pass
- helper: 4/4
- file-handler: 49/49
- email-handler: 12/12
- endpoint-handler: 149/149
Follow-ups:
- Optional: apply equivalent modernization to any remaining relay-backed handlers outside `pages/api/file/` that still use raw axios promise chains and have no explicit phase21 contract assertions.
---
### CL-011: TASK22224 aggressive non-file bundle (email/admin/endpoint parity)
date: 2026-03-23
author: Cline
scope: `pages/api/email/{getmailinglist,getcaseref,notify}.js`, `pages/api/admin/{getnewappeals_api,getlatestdocuments_api}.js`, `pages/api/endpoint/getportallogin_api.js`, `tests/phase21/endpoint-handler-contract.test.cjs`
type: change
rationale: Execute requested aggressive bundling for remaining non-file modernization/parity candidates: remove legacy promise chains and improve hash compatibility on login endpoint while preserving existing contracts.
impact: Improves consistency and resilience across email/admin/endpoint routes with no contract regressions; adds encoded hash-variant compatibility for portal login hash checks.
status: completed
Summary:
- `pages/api/email/getmailinglist.js`
- converted axios `.then/.catch` to `try/catch`
- preserved flattening behavior and error contract `MAILING_LIST_FETCH_FAILED`
- `pages/api/email/getcaseref.js`
- converted axios `.then/.catch` to `try/catch`
- preserved flattening behavior and error contract `CASE_REF_FETCH_FAILED`
- `pages/api/email/notify.js`
- converted notify client `.then/.catch` to `try/catch`
- preserved success payload and error contract `EMAIL_NOTIFY_FAILED`
- `pages/api/admin/getnewappeals_api.js`
- removed unused `CryptoJS` import
- converted axios `.then/.catch` to `try/catch`
- preserved `@odata.nextLink` normalization and error contract `ADMIN_NEW_APPEALS_FETCH_FAILED`
- `pages/api/admin/getlatestdocuments_api.js`
- converted axios `.then/.catch` to `try/catch`
- preserved flatten/enrich behavior and error contract `ADMIN_LATEST_DOCS_FETCH_FAILED`
- `pages/api/endpoint/getportallogin_api.js`
- retained required query/hash guards
- expanded hash validation to accept raw + encoded `emailAddress` query-path candidates
- preserved error contract `PORTAL_LOGIN_FETCH_FAILED`
- phase21 endpoint tests expanded:
- `getportallogin` encoded hash variant success path
- `getnewappeals_api` catch contract
- `getlatestdocuments_api` catch contract
Validation:
- `node tests/phase21/api-contract-slice1.test.cjs` -> pass
- helper: 4/4
- file-handler: 49/49
- email-handler: 12/12
- endpoint-handler: 152/152
Follow-ups:
- Remaining major modernization candidate is `pages/api/file/generateappealpdf.js` (+ optional `pages/api/file/generatepdf.js`) if we continue final closure slices.
---
### CL-012: TASK22224 generatepdf/generateappealpdf async hardening slice
date: 2026-03-23
author: Cline
scope: `pages/api/file/{generateappealpdf,generatepdf}.js`, `tests/phase21/file-handler-contract.test.cjs`
type: change
rationale: Execute next requested slice to modernize remaining file PDF-generation handlers still using promise chains, while preserving existing hash/response behavior.
impact: Improves maintainability and error-path consistency for PDF generation routes; phase21 coverage now includes explicit failure contracts for both handlers.
status: completed
Summary:
- `pages/api/file/generateappealpdf.js`
- converted mixed promise-chain flow to `async/await` + `try/catch`
- preserved existing guard contracts: `MISSING_REQUIRED_QUERY`, `INVALID_HASH`
- preserved generation failure contract: `GENERATE_APPEAL_PDF_FAILED`
- replaced JSX render call with `React.createElement(...)` compatibility form used by test loader
- `pages/api/file/generatepdf.js`
- converted create/upload promise-chain to `async/await` + `try/catch`
- preserved existing guard contracts: `HASH_REQUIRED`, `INVALID_HASH`
- preserved generation failure contract: `GENERATE_PDF_FAILED`
- replaced JSX render call with `React.createElement(...)` compatibility form used by test loader
- phase21 file tests expanded:
- `generatepdf` catch-path contract (`GENERATE_PDF_FAILED`)
- `generateappealpdf` catch-path contract (`GENERATE_APPEAL_PDF_FAILED`)
Validation:
- `node tests/phase21/api-contract-slice1.test.cjs` -> pass
- helper: 4/4
- file-handler: 51/51
- email-handler: 12/12
- endpoint-handler: 152/152
Follow-ups:
- Remaining optional cleanup in these handlers is dead import/unused local pruning (non-behavioral) if we want a final low-risk tidy pass.
---
### CL-013: TASK22224 completion-message route parity closure slice
date: 2026-03-24
author: Cline
scope: `pages/api/file/createappealcompletemessage_api.js`, `tests/phase21/file-handler-contract.test.cjs`
type: change
rationale: Continue next requested slice by closing the final promise-chain parity outlier in file completion-message flow and strengthening phase21 contract coverage.
impact: Improves maintainability and async error hygiene while preserving route behavior and existing error contracts.
status: completed
Summary:
- `pages/api/file/createappealcompletemessage_api.js`
- replaced inline `.catch(...)` on fire-and-forget `updateAccount(...)` with explicit async IIFE + `try/catch` and `void` invocation
- preserved non-blocking behavior and logging semantics for account-update failure path
- preserved primary route contracts and success payload (`{ status: "success" }`)
- phase21 file contract tests expanded:
- success path for encoded hash candidate on `createappealcompletemessage_api`
- dependency-failure contract assertion for `CREATE_APPEAL_COMPLETE_MESSAGE_FAILED`
Validation:
- `node tests/phase21/api-contract-slice1.test.cjs` -> pass
- helper: 4/4
- file-handler: 53/53
- email-handler: 12/12
- endpoint-handler: 152/152
Follow-ups:
- Optional final low-risk tidy sweep: remove dead imports/unused locals in legacy file handlers now that contract hardening stream is functionally complete.
---
### CL-014: TASK22224 pdf render compatibility tidy slice
date: 2026-03-24
author: Cline
scope: `pages/api/file/{generatepdf,generateappealpdf}.js`, `tests/phase21/file-handler-contract.test.cjs`
type: change
rationale: Apply a low-risk compatibility tidy so PDF render invocation remains stable across runtime and contract-test VM contexts.
impact: Keeps functional behavior unchanged while reducing test/runtime mismatch risk in render path setup.
status: completed
Summary:
- `pages/api/file/generatepdf.js`
- switched render call input from `React.createElement(MyDocument, ...)` to direct `MyDocument(...)` invocation in `ReactPDF.renderToStream(...)`
- `pages/api/file/generateappealpdf.js`
- switched render call input from `React.createElement(MyDocument, ...)` to direct `MyDocument(...)` invocation in `ReactPDF.renderToStream(...)`
- `tests/phase21/file-handler-contract.test.cjs`
- added `Buffer` injection for `generatepdf` catch-path test harness to align VM context expectations
Validation:
- `node tests/phase21/api-contract-slice1.test.cjs` -> pass
- helper: 4/4
- file-handler: 53/53
- email-handler: 12/12
- endpoint-handler: 152/152
Follow-ups:
- Optional: dead import cleanup (`Document/Page/Text/View/StyleSheet/PDFViewer`, `middleware`, `nextConnect`, `fs`, etc.) can be done in a dedicated non-behavioral hygiene PR.
---
### CL-015: TASK22224 pdf handler dead-code hygiene slice
date: 2026-03-24
author: Cline
scope: `pages/api/file/{generatepdf,generateappealpdf}.js`
type: change
rationale: Execute the requested next low-risk slice by removing dead imports and unused locals in recently hardened PDF handlers.
impact: Non-behavioral maintainability cleanup; reduces lint noise and future edit risk while preserving existing contracts.
status: completed
Summary:
- `pages/api/file/generatepdf.js`
- pruned unused Azure storage imports, leaving only `createRepPDFBlob`
- pruned unused `@react-pdf/renderer` named imports
- removed unused imports (`middleware`, `nextConnect`, `fs`)
- removed unused locals (`casefolderID`, `representationType`, `repRaiser`, `localeSelect`, `repCapacity`, `repType`)
- `pages/api/file/generateappealpdf.js`
- pruned unused Azure storage imports to only required functions
- pruned unused `@react-pdf/renderer` named imports
- removed unused imports (`middleware`, `nextConnect`, `fs`, `path`, unused pdf templates)
- removed unused local (`caseRef`)
Validation:
- `node tests/phase21/api-contract-slice1.test.cjs` -> pass
- helper: 4/4
- file-handler: 53/53
- email-handler: 12/12
- endpoint-handler: 152/152
Follow-ups:
- Optional: run full repo lint in a separate pass for broader non-slice hygiene now that targeted contract suite is stable.
---
### CL-016: TASK22224 documents download contract slice
date: 2026-03-24
author: Cline
scope: `pages/api/documents/download/[id].js`, `tests/phase21/{documents-handler-contract,api-contract-slice1}.test.cjs`
type: change
rationale: Execute next aggressive slice by standardizing document download guard behavior and bringing the route under phase21 contract coverage.
impact: Improves reliability on invalid input and relay-failure paths while preserving existing user-visible fallback behavior (`/filenotavailable`) for download failures.
status: completed
Summary:
- `pages/api/documents/download/[id].js`
- added explicit required-query guard for `id` and `hash`
- unified fallback redirect path via constant (`/filenotavailable`)
- preserved streaming download behavior and retry flow
- added `tests/phase21/documents-handler-contract.test.cjs` covering:
- missing query -> redirect contract
- success -> attachment/content-type headers + stream pipe contract
- relay failure -> redirect contract
- updated combined runner (`tests/phase21/api-contract-slice1.test.cjs`) to include documents handler contract suite
Validation:
- `node tests/phase21/api-contract-slice1.test.cjs` -> pass
- helper: 4/4
- file-handler: 53/53
- email-handler: 12/12
- endpoint-handler: 152/152
- documents-handler: 3/3
Follow-ups:
- Optional future hardening: migrate documents route onto shared `respondError/respondSuccess` envelope if product requirements allow replacing redirect-style fallback.
---
### CL-017: TASK22224 endpoint legacy-comment hygiene slice
date: 2026-03-24
author: Cline
scope: `pages/api/endpoint/{createwatchedcases_api,getadvancedsearchpaged_api}.js`
type: change
rationale: Complete second requested slice with low-risk maintainability cleanup by removing large obsolete commented legacy handler blocks.
impact: Non-behavioral cleanup only; improves readability and reduces maintenance noise with no runtime contract changes.
status: completed
Summary:
- `createwatchedcases_api.js`
- removed obsolete commented promise-chain implementation block
- `getadvancedsearchpaged_api.js`
- removed obsolete commented legacy implementation block retained below active handler
Validation:
- `node tests/phase21/api-contract-slice1.test.cjs` -> pass
- helper: 4/4
- file-handler: 53/53
- email-handler: 12/12
- endpoint-handler: 152/152
- documents-handler: 3/3
Follow-ups:
- Optional further hygiene pass can target remaining oversized commented historical sections in non-sensitive handlers.
---
### CL-018: TASK22224 nextauth notify micro-refactor
date: 2026-03-24
author: Cline
scope: `pages/api/auth/[...nextauth].js`
type: change
rationale: Execute the explicitly approved auth micro-slice by replacing inline promise `.catch(...)` with explicit `try/catch` while preserving existing auth behavior.
impact: Auth-sensitive non-functional refactor only; keeps current sign-in flow, template/locale routing, and error-handling semantics unchanged.
status: completed
Summary:
- `pages/api/auth/[...nextauth].js`
- replaced:
- `await notifyClient.sendEmail(...).catch((error) => consoleLogger(error))`
- with explicit:
- `try { await notifyClient.sendEmail(...) } catch (error) { consoleLogger(error) }`
- preserved behavior contracts:
- Notify failures are still logged and do not throw through auth handler
- no changes to callback URL construction, locale/template selection, NextAuth options, session/cookies/pages config
Validation:
- `node tests/phase21/api-contract-slice1.test.cjs` -> pass
- helper: 4/4
- file-handler: 53/53
- email-handler: 12/12
- endpoint-handler: 152/152
- documents-handler: 3/3
Follow-ups:
- Optional future auth hygiene (separate guarded slice): replace verbose auth `console.log` diagnostics with structured logger usage once production logging requirements are confirmed.
+22
View File
@@ -32,6 +32,28 @@ Last updated: 2026-03-12
- **Target outcome:** Shared guard middleware for hash/auth/input validation + consistent negative-path responses.
- **Initial scope:** enforce a common pre-handler contract on high-risk routes first.
### Priority 4 — Current slice status (2026-03-23)
- Completed on `TASK22224-file-endpoint-contract-bundle`:
- `pages/api/file/downloadblob.js` hardening + hotfix closure
- path normalization compatibility (filename-only + prefixed path)
- encoded/raw hash validation compatibility guard
- Confirmed outcome:
- user-reported download failures resolved
- phase21 file + combined contract suites passing
### Priority 4 — Next recommended slice on this branch
- Target routes:
- `pages/api/file/deleteblob.js`
- `pages/api/file/deleteblobcase.js`
- `pages/api/file/deleteblobrep.js`
- Scope:
- align hash canonicalization strategy with `downloadblob.js`
- enforce explicit `respondError` contracts (`MISSING_REQUIRED_QUERY`, `INVALID_HASH`, route-specific `*_FAILED`)
- preserve existing success payload shapes
- extend `tests/phase21/file-handler-contract.test.cjs` with mixed encoded/raw hash variants and negative paths
## Priority 5 — Establish minimum automated regression baseline
- **Problem:** Limited automated tests for high-risk logic.
+38 -41
View File
@@ -43,18 +43,18 @@ const encryptDocReference = (documentRef) => {
};
export default async function ApiProxy(req, res) {
var pageNumber = req.query.pageNumber || 1;
var token = await getToken();
const pageNumber = req.query.pageNumber || 1;
const token = await getToken();
var orderby = req.query.orderby || "createdon";
var fieldSort = req.query.fieldSort || "desc";
var showNumberOfRecords = req.query.showNumberOfRecords || 10;
var documentType = req.query.documentType || "all";
var documentOrigin = req.query.documentOrigin || "all";
var numberOfWeeks = req.query.numberWeeks || 1;
const orderby = req.query.orderby || "createdon";
const fieldSort = req.query.fieldSort || "desc";
const showNumberOfRecords = req.query.showNumberOfRecords || 10;
const documentType = req.query.documentType || "all";
const documentOrigin = req.query.documentOrigin || "all";
const numberOfWeeks = req.query.numberWeeks || 1;
var docTypeQueryString = "";
var docOriginQueryString = "";
let docTypeQueryString = "";
let docOriginQueryString = "";
if (documentType != "all") {
if (documentType.indexOf(",") >= 0) {
@@ -108,7 +108,7 @@ export default async function ApiProxy(req, res) {
return resultDate.toISOString().replace(/\.000Z$/, "Z"); // format as 'YYYY-MM-DDT00:00:00Z'
}
var queryUrl =
const queryUrl =
"pinswg_documents?$select=pinswg_name,createdon,pinswg_publishtoweb,_pinswg_documentids_value,pinswg_isharedocumentlocations,pinswg_isharedocumentreference,pinswg_uploadurl,pinswg_uploadstatus,pinswg_origin&$filter=createdon ge " +
daysAgoISO(numberOfWeeks) +
docTypeQueryString +
@@ -131,40 +131,37 @@ export default async function ApiProxy(req, res) {
"\n==========================================\n"
);
var apiResponse = axios
.get(
try {
const { data } = await axios.get(
WEBAPI_URL + queryUrl + hashAPIPath(queryUrl),
azureHeadersPagedCustom(token.access_token, showNumberOfRecords)
)
.then(({ data }) => {
let flattened = data.value.map((r) => ({
...r,
ticketnumber: r.pinswg_DocumentIds?.ticketnumber || null,
publishtoweb: r.pinswg_DocumentIds?.pinswg_publishtoweb || null
}));
);
data.value = flattened;
data.value.forEach(function (element) {
element.pinswg_hashlink = encryptDocReference(
element.pinswg_isharedocumentreference
);
});
const flattened = data.value.map((r) => ({
...r,
ticketnumber: r.pinswg_DocumentIds?.ticketnumber || null,
publishtoweb: r.pinswg_DocumentIds?.pinswg_publishtoweb || null
}));
var dataStr;
_.has(data, "@odata.nextLink") == true &&
((dataStr = JSON.stringify(data["@odata.nextLink"])),
(data["@odata.nextLink"] = dataStr.split("/v8.2/")[1]));
return respondSuccess(res, data);
})
.catch((error) => {
consoleLogger(error);
return respondError(res, {
status: 400,
code: "ADMIN_LATEST_DOCS_FETCH_FAILED",
message: "Failed to fetch latest documents"
});
data.value = flattened;
data.value.forEach(function (element) {
element.pinswg_hashlink = encryptDocReference(
element.pinswg_isharedocumentreference
);
});
return apiResponse;
let dataStr;
_.has(data, "@odata.nextLink") == true &&
((dataStr = JSON.stringify(data["@odata.nextLink"])),
(data["@odata.nextLink"] = dataStr.split("/v8.2/")[1]));
return respondSuccess(res, data);
} catch (error) {
consoleLogger(error);
return respondError(res, {
status: 400,
code: "ADMIN_LATEST_DOCS_FETCH_FAILED",
message: "Failed to fetch latest documents"
});
}
}
+22 -27
View File
@@ -16,7 +16,6 @@
*/
import axios from "axios";
import CryptoJS from "crypto-js";
import _ from "lodash";
import { azureHeadersPagedCustom } from "../../../actions/core/headers";
import { getToken } from "../../../actions/core/token";
@@ -29,15 +28,14 @@ const WEBAPI_URL =
"https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/";
export default async function ApiProxy(req, res) {
var searchString = req.query.searchString;
var pageNumber = req.query.pageNumber || 1;
var token = await getToken();
const pageNumber = req.query.pageNumber || 1;
const token = await getToken();
var orderby = req.query.orderby || "createdon";
var fieldSort = req.query.fieldSort || "desc";
var showNumberOfRecords = req.query.showNumberOfRecords || 10;
const orderby = req.query.orderby || "createdon";
const fieldSort = req.query.fieldSort || "desc";
const showNumberOfRecords = req.query.showNumberOfRecords || 10;
var queryUrl =
const queryUrl =
"incidents?$select=caseorigincode,pinswg_publishtoweb,modifiedon,description,numberofchildincidents,_accountid_value,_customerid_value,_pinswg_associatedlpa_value,_ownerid_value,pinswg_appealcasetype,statuscode,ticketnumber,title, _primarycontactid_value,pinswg_lpareference,pinswg_appellantagent,pinswg_appellantfirstname,pinswg_appellantlastname&$expand=primarycontactid($select=fullname)&$orderby=" +
orderby +
" " +
@@ -55,26 +53,23 @@ export default async function ApiProxy(req, res) {
"\n==========================================\n"
);
var apiResponse = axios
.get(
try {
const { data } = await axios.get(
WEBAPI_URL + queryUrl + hashAPIPath(queryUrl),
azureHeadersPagedCustom(token.access_token, showNumberOfRecords)
)
.then(({ data }) => {
var dataStr;
_.has(data, "@odata.nextLink") == true &&
((dataStr = JSON.stringify(data["@odata.nextLink"])),
(data["@odata.nextLink"] = dataStr.split("/v8.2/")[1]));
return respondSuccess(res, data);
})
.catch((error) => {
consoleLogger(error);
return respondError(res, {
status: 400,
code: "ADMIN_NEW_APPEALS_FETCH_FAILED",
message: "Failed to fetch new appeals"
});
});
);
return apiResponse;
let dataStr;
_.has(data, "@odata.nextLink") == true &&
((dataStr = JSON.stringify(data["@odata.nextLink"])),
(data["@odata.nextLink"] = dataStr.split("/v8.2/")[1]));
return respondSuccess(res, data);
} catch (error) {
consoleLogger(error);
return respondError(res, {
status: 400,
code: "ADMIN_NEW_APPEALS_FETCH_FAILED",
message: "Failed to fetch new appeals"
});
}
}
+6 -4
View File
@@ -139,8 +139,8 @@ const authOptions = (req, res) => {
process.env.NOTIFY_API_KEY
);
await notifyClient
.sendEmail(
try {
await notifyClient.sendEmail(
effectiveLocale === "cy"
? templateIdcy
: templateId,
@@ -149,8 +149,10 @@ const authOptions = (req, res) => {
personalisation,
reference
}
)
.catch((error) => consoleLogger(error));
);
} catch (error) {
consoleLogger(error);
}
}
})
],
+16 -2
View File
@@ -116,6 +116,8 @@ const WEBAPI_URL =
process.env.RELAY_ROOT ||
"https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/";
const FILE_NOT_AVAILABLE_PATH = "/filenotavailable";
// Retry utility with logging
async function retry(fn, retries = 3, delay = 1000) {
for (let attempt = 1; attempt <= retries; attempt++) {
@@ -133,6 +135,18 @@ async function retry(fn, retries = 3, delay = 1000) {
const ApiProxy = async (req, res) => {
const docRef = req.query;
if (
typeof docRef?.id !== "string" ||
docRef.id.length === 0 ||
typeof docRef?.hash !== "string" ||
docRef.hash.length === 0
) {
if (!res.headersSent) {
res.redirect(FILE_NOT_AVAILABLE_PATH);
}
return;
}
try {
const token = await getToken();
const startTime = Date.now();
@@ -186,11 +200,11 @@ const ApiProxy = async (req, res) => {
response.data.on("error", (err) => {
consoleLogger(err);
if (!res.headersSent) res.redirect("/filenotavailable");
if (!res.headersSent) res.redirect(FILE_NOT_AVAILABLE_PATH);
});
} catch (error) {
consoleLogger(error);
if (!res.headersSent) res.redirect("/filenotavailable");
if (!res.headersSent) res.redirect(FILE_NOT_AVAILABLE_PATH);
}
};
+14 -16
View File
@@ -22,27 +22,25 @@ function flattenWatchlistEntry(entry) {
}
export default async function ApiProxy(req, res) {
var token = await getToken();
const token = await getToken();
var queryUrl =
const queryUrl =
"pinswg_watchlists?$count=true&$filter=pinswg_emailnotifications ne null&$expand=pinswg_Contact($select=contactid,emailaddress1)&$select=pinswg_emailnotifications,_pinswg_watchedcase_value";
return axios
.get(
try {
const { data } = await axios.get(
WEBAPI_URL + queryUrl + hashAPIPath(queryUrl),
azureHeaders(token.access_token)
)
.then(({ data }) => {
const flattenedResults = data.value.map(flattenWatchlistEntry);
);
const flattenedResults = data.value.map(flattenWatchlistEntry);
return respondSuccess(res, flattenedResults);
})
.catch((error) => {
consoleLogger(error);
return respondError(res, {
status: 400,
code: "CASE_REF_FETCH_FAILED",
message: "Failed to fetch case references"
});
return respondSuccess(res, flattenedResults);
} catch (error) {
consoleLogger(error);
return respondError(res, {
status: 400,
code: "CASE_REF_FETCH_FAILED",
message: "Failed to fetch case references"
});
}
}
+14 -16
View File
@@ -22,27 +22,25 @@ function flattenWatchlistEntry(entry) {
}
export default async function ApiProxy(req, res) {
var token = await getToken();
const token = await getToken();
var queryUrl =
const queryUrl =
"pinswg_watchlists?$count=true&$filter=pinswg_emailnotifications ne null&$expand=pinswg_Contact($select=contactid,emailaddress1)&$select=pinswg_emailnotifications,_pinswg_watchedcase_value";
return axios
.get(
try {
const { data } = await axios.get(
WEBAPI_URL + queryUrl + hashAPIPath(queryUrl),
azureHeaders(token.access_token)
)
.then(({ data }) => {
const flattenedResults = data.value.map(flattenWatchlistEntry);
);
const flattenedResults = data.value.map(flattenWatchlistEntry);
return respondSuccess(res, flattenedResults);
})
.catch((error) => {
consoleLogger(error);
return respondError(res, {
status: 400,
code: "MAILING_LIST_FETCH_FAILED",
message: "Failed to fetch mailing list"
});
return respondSuccess(res, flattenedResults);
} catch (error) {
consoleLogger(error);
return respondError(res, {
status: 400,
code: "MAILING_LIST_FETCH_FAILED",
message: "Failed to fetch mailing list"
});
}
}
+14 -15
View File
@@ -31,7 +31,7 @@ import { getPreferredLanguage } from "../../../actions/services/accountService";
import { respondError, respondSuccess } from "../middleware/apiResponse";
export default async function ApiProxy(req, res) {
var data = req.body;
const data = req.body;
const emailAddress = sanitizeString(data?.emailAddress);
if (!isNonEmptyString(emailAddress)) {
@@ -58,7 +58,7 @@ export default async function ApiProxy(req, res) {
}
}
var NotifyClient = require("notifications-node-client").NotifyClient;
const NotifyClient = require("notifications-node-client").NotifyClient;
const notifyClient = new NotifyClient(process.env.NOTIFY_API_KEY);
//const emailReplyToId = process.env.EMAIL_REPLY_TO_ID;
@@ -68,20 +68,19 @@ export default async function ApiProxy(req, res) {
payload: redactSensitive(data)
});
notifyClient
.sendEmail(data.templateId, data.emailAddress, {
try {
await notifyClient.sendEmail(data.templateId, data.emailAddress, {
personalisation: data.personalisation,
reference: data.reference
})
.then(() => {
return respondSuccess(res, data);
})
.catch((error) => {
consoleLogger(error);
return respondError(res, {
status: 400,
code: "EMAIL_NOTIFY_FAILED",
message: "Failed to send notify email"
});
});
return respondSuccess(res, data);
} catch (error) {
consoleLogger(error);
return respondError(res, {
status: 400,
code: "EMAIL_NOTIFY_FAILED",
message: "Failed to send notify email"
});
}
}
@@ -20,35 +20,6 @@ const WEBAPI_URL =
process.env.RELAY_ROOT ||
"https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/";
// export default async function ApiProxy(req, res) {
// var token = await getToken();
// var data = JSON.stringify(req.body);
// var queryUrl = "pinswg_watchlists";
// console.log(data);
// var config = {
// method: "put",
// url: WEBAPI_URL + queryUrl + hashAPIPath(queryUrl),
// headers: {
// "OData-MaxVersion": "4.0",
// "OData-Version": "4.0",
// "Accept": "application/json",
// "Prefer": 'odata.include-annotations="*",return=representation',
// "Authorization": "Bearer " + token.access_token,
// "Content-Type": "application/json",
// },
// data: data,
// };
// return axios(config)
// .then(({ data }) => {
// res.status(200).json(data);
// })
// .catch((error) => {
// consoleLogger(Object.assign(err, data));
// res.status(400).json(error);
// });
// }
const recordExists = async (incidentId, contactId, token) => {
const filter = `$filter=pinswg_WatchedCase/incidentid eq ${incidentId} and pinswg_Contact/contactid eq ${contactId}`;
const queryUrl = `pinswg_watchlists?${filter}`;
@@ -258,161 +258,3 @@ export default async function ApiProxy(req, res) {
});
}
}
// export default async function ApiProxy(req, res) {
// var searchString = req.query.searchstring;
// var pageNumber = req.query.pageNumber;
// var token = await getToken();
// var orderby = req.query.orderby;
// var fieldSort = req.query.fieldSort;
// var showNumberOfRecords = req.query.showNumberOfRecords;
// searchString = _.isEmpty(searchString)
// ? searchString
// : JSON.parse(decodeURI(searchString));
// var queryString = "";
// queryString +=
// _.has(searchString, "q") && searchString.q != null
// ? "(contains(title, '" +
// searchString.q.replace(/\'/g, "''") +
// "') or contains(ticketnumber,'" +
// searchString.q.replace(/\'/g, "''") +
// "')) and"
// : "";
// queryString +=
// (_.has(searchString, "lpa") || _.has(searchString, "LPA")) &&
// searchString.lpa != null
// ? " _pinswg_associatedlpa_value eq " + searchString.lpa + " and"
// : "";
// queryString +=
// _.has(searchString, "apt") && searchString.apt != null
// ? " pinswg_appealcasetype eq " + searchString.apt + " and"
// : "";
// queryString +=
// _.has(searchString, "statuscode") && searchString.statuscode != null
// ? " statuscode eq " + searchString.statuscode + " and"
// : "";
// var queryUrl =
// "incidents?$select=pinswg_environmentalstatementlocation,pinswg_caseaddress,modifiedon,description,numberofchildincidents,_accountid_value,_customerid_value,_pinswg_associatedlpa_value,_ownerid_value,pinswg_appealcasetype,statuscode,ticketnumber,title,_primarycontactid_value,pinswg_lpareference,pinswg_appellantagent,pinswg_appellantfirstname,pinswg_appellantlastname&$expand=primarycontactid($select=fullname)&$filter=" +
// queryString +
// " pinswg_appealcasetype ne null and pinswg_publishtoweb eq true&$orderby=" +
// orderby +
// " " +
// fieldSort +
// "&$count=true";
// console.log(WEBAPI_URL + queryUrl + hashAPIPath(queryUrl));
// var apiResponse = _.isEmpty(searchString)
// ? res.status(400).json()
// : axios
// .get(
// WEBAPI_URL + queryUrl + hashAPIPath(queryUrl),
// azureHeadersPagedCustom(
// token.access_token,
// showNumberOfRecords
// )
// )
// .then(async ({ data }) => {
// var dataStr;
// _.has(data, "@odata.nextLink") == true &&
// ((dataStr = JSON.stringify(data["@odata.nextLink"])),
// (data["@odata.nextLink"] = dataStr.split("/v8.2/")[1]));
// if (_.has(searchString, "projecttype")) {
// await updateValueArray(data, token);
// async function updateValueArray(data, token) {
// let hasNextPage = true;
// let nextPageUrl = data["@odata.nextLink"];
// // Loop through all pages
// while (hasNextPage) {
// // Iterate through current data and process it
// for (let i = 0; i < data.value.length; i++) {
// const item = data.value[i];
// try {
// // Axios call using the `incidentid` to fetch additional data
// const response = await axios.get(
// WEBAPI_URL +
// "pinswg_sipses?$filter=_pinswg_sipscase_value eq " +
// item.incidentid +
// " and _pinswg_projecttype_value eq " +
// searchString.projecttype +
// "&$select=_pinswg_projecttype_value" +
// hashAPIPath(
// "pinswg_sipses?$filter=_pinswg_sipscase_value eq " +
// item.incidentid +
// " and _pinswg_projecttype_value eq " +
// searchString.projecttype +
// "&$select=_pinswg_projecttype_value"
// ),
// azureHeadersPaged(token.access_token)
// );
// if (
// response.data.value.length > 0 &&
// response.data.value[0]
// ._pinswg_projecttype_value != null
// ) {
// Object.assign(
// item,
// response.data.value[0]
// ); // Update item with new data
// } else {
// console.log(
// `No project type found for incident ID ${item.incidentid}`
// );
// }
// } catch (error) {
// console.error(
// `Error fetching data for incident ID ${item.incidentid}:`,
// error
// );
// }
// }
// // If there's a next page, fetch it
// if (nextPageUrl) {
// const nextPageData = await axios.get(
// WEBAPI_URL +
// nextPageUrl +
// hashAPIPath(nextPageUrl),
// azureHeadersPaged(token.access_token)
// );
// data.value = [
// ...data.value,
// ...nextPageData.data.value,
// ];
// nextPageUrl =
// nextPageData.data["@odata.nextLink"];
// } else {
// hasNextPage = false;
// }
// }
// }
// data.value = data.value.filter(
// (item) => item._pinswg_projecttype_value != null
// );
// data["@odata.count"] = data.value.length;
// }
// res.status(200).json(data);
// })
// .catch((error) => {
// consoleLogger(error);
// res.status(400).json(error);
// });
// return apiResponse;
// }
@@ -1,4 +1,4 @@
import { respondSuccess } from "../middleware/apiResponse";
import { respondError, respondSuccess } from "../middleware/apiResponse";
/**
* @swagger
@@ -13,6 +13,14 @@ import { respondSuccess } from "../middleware/apiResponse";
*/
const ApiResponse = (req, res) => {
if (req.method && req.method !== "GET") {
return respondError(res, {
status: 405,
code: "METHOD_NOT_ALLOWED",
message: "Only GET is supported"
});
}
return respondSuccess(res, {
"value": [
{
+9 -1
View File
@@ -49,8 +49,16 @@ export default async function ApiProxy(req, res) {
const checkquerypath =
"/api/endpoint/getportallogin_api?emailAddress=" + emailAddress;
const encodedCheckquerypath =
"/api/endpoint/getportallogin_api?emailAddress=" +
encodeURIComponent(emailAddress);
if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) {
const expectedHashCandidates = [
hashAPIPath(checkquerypath),
hashAPIPath(encodedCheckquerypath)
];
if (!expectedHashCandidates.includes("&hash=" + checkHash)) {
return respondError(res, {
status: 400,
code: "INVALID_HASH",
+9 -24
View File
@@ -1,29 +1,14 @@
import { respondSuccess } from "../middleware/apiResponse";
import { respondError, respondSuccess } from "../middleware/apiResponse";
// export default async function ApiProxy(req, res) {
// var caseid = req.query.caseid;
// var token = await getToken();
export default function ApiProxy(req, res) {
if (req.method && req.method !== "GET") {
return respondError(res, {
status: 405,
code: "METHOD_NOT_ALLOWED",
message: "Only GET is supported"
});
}
// var queryUrl =
// "pinswg_sipsevents?$filter=_pinswg_sipseventsid_value eq " +
// caseid +
// "&$count=true";
// return axios
// .get(
// WEBAPI_URL + queryUrl + hashAPIPath(queryUrl),
// azureHeaders(token.access_token),
// )
// .then(({ data }) => {
// res.status(200).json(data);
// })
// .catch((error) => {
// consoleLogger(error);
// res.status(400).json(error);
// });
// }
export default function ApiProx(req, res) {
const data = {
"@odata.count": 4,
"value": [
@@ -18,10 +18,10 @@ const ApiProxy = nextConnect();
ApiProxy.use(middleware);
ApiProxy.get(async (req, res) => {
var containerName = req.query.container;
var tempCaseRef = req.query.tempcaseref;
var typeofinvolvement = req.query.inv;
var checkHash = req.query.hash;
const containerName = req.query.container;
const tempCaseRef = req.query.tempcaseref;
const typeofinvolvement = req.query.inv;
const checkHash = req.query.hash;
if (
typeof containerName === "undefined" ||
@@ -38,13 +38,22 @@ ApiProxy.get(async (req, res) => {
});
}
var checkquerypath =
const hashCandidatePaths = [
"/api/file/createappealcompletemessage_api?container=" +
containerName +
"&tempcaseref=" +
tempCaseRef;
containerName +
"&tempcaseref=" +
tempCaseRef,
"/api/file/createappealcompletemessage_api?container=" +
encodeURIComponent(containerName) +
"&tempcaseref=" +
encodeURIComponent(tempCaseRef)
];
if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) {
const isHashValid = hashCandidatePaths.some(
(candidatePath) => hashAPIPath(candidatePath) == "&hash=" + checkHash
);
if (!isHashValid) {
return respondError(res, {
status: 400,
code: "INVALID_HASH",
@@ -52,83 +61,87 @@ ApiProxy.get(async (req, res) => {
});
}
const blobProgress = await getProgressBlobs(
containerName,
tempCaseRef
).then((data) => {
return downloadProgressFile(containerName, data.path, tempCaseRef);
});
try {
const progressBlob = await getProgressBlobs(containerName, tempCaseRef);
const blobProgress = await downloadProgressFile(
containerName,
progressBlob.path,
tempCaseRef
);
const caseObj = await downloadProgressFile(
containerName,
tempCaseRef + "/" + tempCaseRef + "_case.json",
tempCaseRef
);
const caseObj = await downloadProgressFile(
containerName,
tempCaseRef + "/" + tempCaseRef + "_case.json",
tempCaseRef
);
Object.assign(blobProgress, {
"appealComplete": tempCaseRef
});
Object.assign(blobProgress, {
appealComplete: tempCaseRef
});
delete blobProgress["pinswg_name"];
delete blobProgress.pinswg_name;
await createBlob(JSON.stringify(blobProgress), containerName, tempCaseRef)
.then(() => {
//update case.json with lpa ref and description
await createBlob(
JSON.stringify(blobProgress),
containerName,
tempCaseRef
);
let combinedAddress =
blobProgress.pinswg_siteaddressline1 +
(!_.isEmpty(blobProgress.pinswg_siteaddressline2)
? ", " + blobProgress.pinswg_siteaddressline2
: "") +
(!_.isEmpty(blobProgress.pinswg_siteaddresstown)
? ", " + blobProgress.pinswg_siteaddresstown
: "");
let combinedAddress =
blobProgress.pinswg_siteaddressline1 +
(!_.isEmpty(blobProgress.pinswg_siteaddressline2)
? ", " + blobProgress.pinswg_siteaddressline2
: "") +
(!_.isEmpty(blobProgress.pinswg_siteaddresstown)
? ", " + blobProgress.pinswg_siteaddresstown
: "");
combinedAddress =
combinedAddress +
", " +
blobProgress.pinswg_siteaddresscounty +
", " +
blobProgress.pinswg_siteaddresspostcode;
combinedAddress =
combinedAddress +
", " +
blobProgress.pinswg_siteaddresscounty +
", " +
blobProgress.pinswg_siteaddresspostcode;
Object.assign(caseObj, {
"pinswg_lpareference":
blobProgress.pinswg_lpaapplicationreference,
"description": blobProgress.pinswg_developmentdescription,
"pinswg_caseaddress": combinedAddress
});
Object.assign(caseObj, {
pinswg_lpareference: blobProgress.pinswg_lpaapplicationreference,
description: blobProgress.pinswg_developmentdescription,
pinswg_caseaddress: combinedAddress
});
//send update case.json and then create queue message
getCaseBlob(containerName, tempCaseRef, caseObj).then(() => {
let contactId = caseObj["customerid_contact@odata.bind"];
contactId = contactId.match(/\(([^)]+)\)/);
await getCaseBlob(containerName, tempCaseRef, caseObj);
if (typeofinvolvement != 846040000) {
updateAccount(
let contactId = caseObj["customerid_contact@odata.bind"];
contactId = contactId.match(/\(([^)]+)\)/);
if (typeofinvolvement != 846040000) {
void (async () => {
try {
await updateAccount(
contactId[1],
{
"pinswg_typeofinvolvement": 846040001
pinswg_typeofinvolvement: 846040001
},
true
).catch((error) => {
consoleLogger(error);
});
);
} catch (error) {
consoleLogger(error);
}
})();
}
createCaseCompleteMessage(containerName, tempCaseRef);
return respondSuccess(res, {
status: "success"
});
});
})
.catch((error) => {
consoleLogger(error);
return respondError(res, {
status: 400,
code: "CREATE_APPEAL_COMPLETE_MESSAGE_FAILED",
message: "Failed to create appeal complete message"
});
await createCaseCompleteMessage(containerName, tempCaseRef);
return respondSuccess(res, {
status: "success"
});
} catch (error) {
consoleLogger(error);
return respondError(res, {
status: 400,
code: "CREATE_APPEAL_COMPLETE_MESSAGE_FAILED",
message: "Failed to create appeal complete message"
});
}
});
export const config = {
@@ -17,8 +17,8 @@ const hasValue = (value) =>
typeof value === "string" && value.trim().length > 0;
ApiProxy.get(async (req, res) => {
var containerName = req.query.container;
var tempCaseRef = req.query.tempcaseref;
const containerName = req.query.container;
const tempCaseRef = req.query.tempcaseref;
if (!hasValue(containerName) || !hasValue(tempCaseRef)) {
return respondError(res, {
@@ -28,30 +28,28 @@ ApiProxy.get(async (req, res) => {
});
}
var token = await getToken();
const token = await getToken();
var queryUrl =
const queryUrl =
"/api/file/createappealcompletemessage_api?container=" +
containerName +
encodeURIComponent(containerName) +
"&tempcaseref=" +
tempCaseRef;
encodeURIComponent(tempCaseRef);
return axios
.get(
try {
const { data } = await axios.get(
BASE_URL + queryUrl + hashAPIPath(queryUrl),
azureHeaders(token.access_token)
)
.then(({ data }) => {
return respondSuccess(res, data);
})
.catch((error) => {
consoleLogger(error);
return respondError(res, {
status: 400,
code: "CREATE_APPEAL_COMPLETE_MESSAGE_PROXY_FAILED",
message: "Failed to create appeal complete message"
});
);
return respondSuccess(res, data);
} catch (error) {
consoleLogger(error);
return respondError(res, {
status: 400,
code: "CREATE_APPEAL_COMPLETE_MESSAGE_PROXY_FAILED",
message: "Failed to create appeal complete message"
});
}
});
export const config = {
+21 -20
View File
@@ -11,7 +11,6 @@
*/
import axios from "axios";
import CryptoJS from "crypto-js";
import { consoleLogger } from "../../../actions/core/logger";
import { getToken } from "../../../actions/core/token";
import { hashAPIPath } from "../../../actions/core/hash";
@@ -36,20 +35,20 @@ export default async function ApiProxy(req, res) {
});
}
var token = await getToken();
var contactid = req.body.contactid;
var queryUrl =
const token = await getToken();
const contactid = req.body.contactid;
const queryUrl =
"incidents(" +
req.body.incidentid +
")/pinswg_incident_contact_case_involvement/$ref";
var crmUrl = "https://" + process.env.CRMURL;
const crmUrl = "https://" + process.env.CRMURL;
var data = {
const data = {
"@odata.id": crmUrl + "/api/data/v8.2/contacts(" + contactid + ")"
};
var config = {
const config = {
method: "post",
url: WEBAPI_URL + queryUrl + hashAPIPath(queryUrl),
headers: {
@@ -64,18 +63,20 @@ export default async function ApiProxy(req, res) {
data: data
};
return axios(config)
.then(({ data }) => {
return respondSuccess(res, data);
})
.catch((error) => {
return error.status == 412
? respondSuccess(res, { "record": "exists" })
: (consoleLogger(Object.assign(error, data)),
respondError(res, {
status: 400,
code: "CREATE_CASE_INVOLVEMENT_FAILED",
message: "Failed to create case involvement"
}));
try {
const { data: responseData } = await axios(config);
return respondSuccess(res, responseData);
} catch (error) {
const errorStatus = error?.status || error?.response?.status;
if (errorStatus == 412) {
return respondSuccess(res, { record: "exists" });
}
consoleLogger(Object.assign(error, data));
return respondError(res, {
status: 400,
code: "CREATE_CASE_INVOLVEMENT_FAILED",
message: "Failed to create case involvement"
});
}
}
+36 -22
View File
@@ -13,10 +13,10 @@ const ApiProxy = nextConnect();
ApiProxy.use(middleware);
ApiProxy.get(async (req, res) => {
var containerName = req.query.container;
var tempCaseRef = req.query.tempcaseref;
var filename = req.query.repid;
var checkHash = req.query.hash;
const containerName = req.query.container;
const tempCaseRef = req.query.tempcaseref;
const filename = req.query.repid;
const checkHash = req.query.hash;
if (
typeof containerName === "undefined" ||
@@ -35,15 +35,26 @@ ApiProxy.get(async (req, res) => {
});
}
var checkquerypath =
const hashCandidatePaths = [
"/api/file/createrepcompletemessage_api?container=" +
containerName +
"&tempcaseref=" +
tempCaseRef +
"&repid=" +
filename;
containerName +
"&tempcaseref=" +
tempCaseRef +
"&repid=" +
filename,
"/api/file/createrepcompletemessage_api?container=" +
encodeURIComponent(containerName) +
"&tempcaseref=" +
encodeURIComponent(tempCaseRef) +
"&repid=" +
encodeURIComponent(filename)
];
if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) {
const isHashValid = hashCandidatePaths.some(
(candidatePath) => hashAPIPath(candidatePath) == "&hash=" + checkHash
);
if (!isHashValid) {
return respondError(res, {
status: 400,
code: "INVALID_HASH",
@@ -51,18 +62,21 @@ ApiProxy.get(async (req, res) => {
});
}
await createRepCompleteMessage(containerName, tempCaseRef, filename)
.then((data) => {
return respondSuccess(res, data);
})
.catch((error) => {
consoleLogger(error);
return respondError(res, {
status: 400,
code: "CREATE_REP_COMPLETE_MESSAGE_FAILED",
message: "Failed to create representation complete message"
});
try {
const data = await createRepCompleteMessage(
containerName,
tempCaseRef,
filename
);
return respondSuccess(res, data);
} catch (error) {
consoleLogger(error);
return respondError(res, {
status: 400,
code: "CREATE_REP_COMPLETE_MESSAGE_FAILED",
message: "Failed to create representation complete message"
});
}
});
export const config = {
+22 -21
View File
@@ -11,7 +11,6 @@
*/
import axios from "axios";
import CryptoJS from "crypto-js";
import { consoleLogger } from "../../../actions/core/logger";
import { getToken } from "../../../actions/core/token";
import { hashAPIPath } from "../../../actions/core/hash";
@@ -36,22 +35,22 @@ export default async function ApiProxy(req, res) {
});
}
var token = await getToken();
var contactid = req.body.contactid;
var queryUrl =
const token = await getToken();
const contactid = req.body.contactid;
const queryUrl =
"incidents(" +
req.body.incidentid +
")/pinswg_incident_contact_case_involvement/$ref";
var crmUrl = "https://" + process.env.CRMURL;
var crmVersion = process.env.CRMURL_VERSION;
const crmUrl = "https://" + process.env.CRMURL;
const crmVersion = process.env.CRMURL_VERSION;
var data = {
const data = {
"@odata.id":
crmUrl + "/api/data/" + crmVersion + "/contacts(" + contactid + ")"
};
var config = {
const config = {
method: "post",
url: WEBAPI_URL + queryUrl + hashAPIPath(queryUrl),
headers: {
@@ -66,18 +65,20 @@ export default async function ApiProxy(req, res) {
data: data
};
return axios(config)
.then(({ data }) => {
return respondSuccess(res, data);
})
.catch((error) => {
return error.status == 412
? respondSuccess(res, { "record": "exists" })
: (consoleLogger(Object.assign(error, data)),
respondError(res, {
status: 400,
code: "CREATE_REP_INVOLVEMENT_FAILED",
message: "Failed to create representation involvement"
}));
try {
const { data: responseData } = await axios(config);
return respondSuccess(res, responseData);
} catch (error) {
const errorStatus = error?.status || error?.response?.status;
if (errorStatus == 412) {
return respondSuccess(res, { record: "exists" });
}
consoleLogger(Object.assign(error, data));
return respondError(res, {
status: 400,
code: "CREATE_REP_INVOLVEMENT_FAILED",
message: "Failed to create representation involvement"
});
}
}
@@ -9,10 +9,10 @@ const ApiProxy = nextConnect();
ApiProxy.use(middleware);
ApiProxy.get(async (req, res) => {
var containerName = req.query.container;
var casefolderID = req.query.casefolderID;
var blobName = req.query.blobname;
var checkHash = req.query.hash;
const containerName = req.query.container;
const casefolderID = req.query.casefolderID;
const blobName = req.query.blobname;
const checkHash = req.query.hash;
if (
typeof containerName === "undefined" ||
@@ -31,15 +31,43 @@ ApiProxy.get(async (req, res) => {
});
}
var checkquerypath =
"/api/file/deleteblob?container=" +
containerName +
"&casefolderID=" +
casefolderID +
"&blobname=" +
blobName;
const casefolderIDTrimmed = casefolderID.trim();
const blobNameTrimmed = blobName.trim();
if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) {
const hashCandidatePaths = [
"/api/file/deleteawaitingsubmissionfromblob?container=" +
containerName +
"&casefolderID=" +
casefolderIDTrimmed +
"&blobname=" +
blobNameTrimmed,
"/api/file/deleteawaitingsubmissionfromblob?container=" +
containerName +
"&casefolderID=" +
encodeURIComponent(casefolderIDTrimmed) +
"&blobname=" +
encodeURIComponent(blobNameTrimmed),
// legacy compatibility with callers that hash against deleteblob route
"/api/file/deleteblob?container=" +
containerName +
"&casefolderID=" +
casefolderIDTrimmed +
"&blobname=" +
blobNameTrimmed,
"/api/file/deleteblob?container=" +
containerName +
"&casefolderID=" +
encodeURIComponent(casefolderIDTrimmed) +
"&blobname=" +
encodeURIComponent(blobNameTrimmed)
];
const isHashValid = hashCandidatePaths.some(
(candidatePath) => hashAPIPath(candidatePath) == "&hash=" + checkHash
);
if (!isHashValid) {
return respondError(res, {
status: 400,
code: "INVALID_HASH",
@@ -47,11 +75,22 @@ ApiProxy.get(async (req, res) => {
});
}
await deleteBlob(containerName, casefolderID + "/files/" + blobName).then(
(data) => {
return respondSuccess(res, { data: data });
}
);
try {
const normalizedBlobName = blobNameTrimmed.startsWith(
casefolderIDTrimmed + "/"
)
? blobNameTrimmed
: casefolderIDTrimmed + "/files/" + blobNameTrimmed;
const data = await deleteBlob(containerName, normalizedBlobName);
return respondSuccess(res, { data: data });
} catch (error) {
return respondError(res, {
status: 400,
code: "DELETE_AWAITING_SUBMISSION_BLOB_FAILED",
message: "Unable to delete awaiting submission blob"
});
}
});
export const config = {
+54 -17
View File
@@ -9,10 +9,10 @@ const ApiProxy = nextConnect();
ApiProxy.use(middleware);
ApiProxy.get(async (req, res) => {
var containerName = req.query.container;
var casefolderID = req.query.casefolderID;
var blobName = req.query.blobname;
var checkHash = req.query.hash;
const containerName = req.query.container;
const casefolderID = req.query.casefolderID;
const blobName = req.query.blobname;
const checkHash = req.query.hash;
if (
typeof containerName === "undefined" ||
@@ -31,15 +31,41 @@ ApiProxy.get(async (req, res) => {
});
}
var checkquerypath =
"/api/file/deleteblob?container=" +
containerName +
"&casefolderID=" +
encodeURIComponent(casefolderID) +
"&blobname=" +
encodeURIComponent(blobName);
const casefolderIDTrimmed = casefolderID.trim();
const blobNameTrimmed = blobName.trim();
if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) {
const hashCandidatePaths = [
"/api/file/deleteblob?container=" +
containerName +
"&casefolderID=" +
casefolderIDTrimmed +
"&blobname=" +
blobNameTrimmed,
"/api/file/deleteblob?container=" +
containerName +
"&casefolderID=" +
encodeURIComponent(casefolderIDTrimmed) +
"&blobname=" +
blobNameTrimmed,
"/api/file/deleteblob?container=" +
containerName +
"&casefolderID=" +
casefolderIDTrimmed +
"&blobname=" +
encodeURIComponent(blobNameTrimmed),
"/api/file/deleteblob?container=" +
containerName +
"&casefolderID=" +
encodeURIComponent(casefolderIDTrimmed) +
"&blobname=" +
encodeURIComponent(blobNameTrimmed)
];
const isHashValid = hashCandidatePaths.some(
(candidatePath) => hashAPIPath(candidatePath) == "&hash=" + checkHash
);
if (!isHashValid) {
return respondError(res, {
status: 400,
code: "INVALID_HASH",
@@ -47,11 +73,22 @@ ApiProxy.get(async (req, res) => {
});
}
await deleteBlob(containerName, casefolderID + "/files/" + blobName).then(
(data) => {
return respondSuccess(res, { data: data });
}
);
try {
const normalizedBlobName = blobNameTrimmed.startsWith(
casefolderIDTrimmed + "/"
)
? blobNameTrimmed
: casefolderIDTrimmed + "/files/" + blobNameTrimmed;
const data = await deleteBlob(containerName, normalizedBlobName);
return respondSuccess(res, { data: data });
} catch (error) {
return respondError(res, {
status: 400,
code: "DELETE_BLOB_FAILED",
message: "Unable to delete blob"
});
}
});
export const config = {
+29 -11
View File
@@ -9,9 +9,9 @@ const ApiProxy = nextConnect();
ApiProxy.use(middleware);
ApiProxy.get(async (req, res) => {
var containerName = req.query.container;
var casefolderID = req.query.casefolderID;
var checkHash = req.query.hash;
const containerName = req.query.container;
const casefolderID = req.query.casefolderID;
const checkHash = req.query.hash;
if (
typeof containerName === "undefined" ||
@@ -28,13 +28,24 @@ ApiProxy.get(async (req, res) => {
});
}
var checkquerypath =
"/api/file/deleteblobcase?container=" +
containerName +
"&casefolderID=" +
casefolderID;
const casefolderIDTrimmed = casefolderID.trim();
if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) {
const hashCandidatePaths = [
"/api/file/deleteblobcase?container=" +
containerName +
"&casefolderID=" +
casefolderIDTrimmed,
"/api/file/deleteblobcase?container=" +
containerName +
"&casefolderID=" +
encodeURIComponent(casefolderIDTrimmed)
];
const isHashValid = hashCandidatePaths.some(
(candidatePath) => hashAPIPath(candidatePath) == "&hash=" + checkHash
);
if (!isHashValid) {
return respondError(res, {
status: 400,
code: "INVALID_HASH",
@@ -42,9 +53,16 @@ ApiProxy.get(async (req, res) => {
});
}
await deleteBlobCase(containerName, casefolderID).then((data) => {
try {
const data = await deleteBlobCase(containerName, casefolderIDTrimmed);
return respondSuccess(res, { data: data });
});
} catch (error) {
return respondError(res, {
status: 400,
code: "DELETE_BLOB_CASE_FAILED",
message: "Unable to delete blob case"
});
}
});
export const config = {
+48 -16
View File
@@ -9,10 +9,10 @@ const ApiProxy = nextConnect();
ApiProxy.use(middleware);
ApiProxy.get(async (req, res) => {
var containerName = req.query.container;
var casefolderID = req.query.casefolderID;
var repfile = req.query.repfile;
var checkHash = req.query.hash;
const containerName = req.query.container;
const casefolderID = req.query.casefolderID;
const repfile = req.query.repfile;
const checkHash = req.query.hash;
if (
typeof containerName === "undefined" ||
@@ -31,15 +31,41 @@ ApiProxy.get(async (req, res) => {
});
}
var checkquerypath =
"/api/file/deleteblobrep?container=" +
containerName +
"&casefolderID=" +
casefolderID +
"&repfile=" +
repfile;
const casefolderIDTrimmed = casefolderID.trim();
const repfileTrimmed = repfile.trim();
if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) {
const hashCandidatePaths = [
"/api/file/deleteblobrep?container=" +
containerName +
"&casefolderID=" +
casefolderIDTrimmed +
"&repfile=" +
repfileTrimmed,
"/api/file/deleteblobrep?container=" +
containerName +
"&casefolderID=" +
encodeURIComponent(casefolderIDTrimmed) +
"&repfile=" +
repfileTrimmed,
"/api/file/deleteblobrep?container=" +
containerName +
"&casefolderID=" +
casefolderIDTrimmed +
"&repfile=" +
encodeURIComponent(repfileTrimmed),
"/api/file/deleteblobrep?container=" +
containerName +
"&casefolderID=" +
encodeURIComponent(casefolderIDTrimmed) +
"&repfile=" +
encodeURIComponent(repfileTrimmed)
];
const isHashValid = hashCandidatePaths.some(
(candidatePath) => hashAPIPath(candidatePath) == "&hash=" + checkHash
);
if (!isHashValid) {
return respondError(res, {
status: 400,
code: "INVALID_HASH",
@@ -47,11 +73,17 @@ ApiProxy.get(async (req, res) => {
});
}
casefolderID = casefolderID + "/" + repfile;
await deleteBlobRep(containerName, casefolderID).then((data) => {
try {
const normalizedRepPath = casefolderIDTrimmed + "/" + repfileTrimmed;
const data = await deleteBlobRep(containerName, normalizedRepPath);
return respondSuccess(res, { data: data });
});
} catch (error) {
return respondError(res, {
status: 400,
code: "DELETE_BLOB_REP_FAILED",
message: "Unable to delete blob representation"
});
}
});
export const config = {
+61 -35
View File
@@ -10,10 +10,10 @@ const ApiProxy = nextConnect();
ApiProxy.use(middleware);
ApiProxy.get(async (req, res) => {
var containerName = req.query.container;
var casefolderID = req.query.casefolderID;
var blobName = req.query.blobname;
var checkHash = req.query.hash;
const containerName = req.query.container;
const casefolderID = req.query.casefolderID;
const blobName = req.query.blobname;
const checkHash = req.query.hash;
if (
typeof containerName === "undefined" ||
@@ -32,15 +32,39 @@ ApiProxy.get(async (req, res) => {
});
}
var checkquerypath =
const blobNameTrimmed = blobName.trim();
const hashCandidatePaths = [
"/api/file/downloadblob?container=" +
containerName +
"&casefolderID=" +
casefolderID +
"&blobname=" +
blobName.trim();
containerName +
"&casefolderID=" +
casefolderID +
"&blobname=" +
blobNameTrimmed,
"/api/file/downloadblob?container=" +
containerName +
"&casefolderID=" +
encodeURIComponent(casefolderID) +
"&blobname=" +
blobNameTrimmed,
"/api/file/downloadblob?container=" +
containerName +
"&casefolderID=" +
casefolderID +
"&blobname=" +
encodeURIComponent(blobNameTrimmed),
"/api/file/downloadblob?container=" +
containerName +
"&casefolderID=" +
encodeURIComponent(casefolderID) +
"&blobname=" +
encodeURIComponent(blobNameTrimmed)
];
if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) {
const isHashValid = hashCandidatePaths.some(
(candidatePath) => hashAPIPath(candidatePath) == "&hash=" + checkHash
);
if (!isHashValid) {
return respondError(res, {
status: 400,
code: "INVALID_HASH",
@@ -48,34 +72,36 @@ ApiProxy.get(async (req, res) => {
});
}
const bloblocation =
casefolderID + (blobName.indexOf(".json") > 0 ? "/" : "/files/");
try {
const decodedBlobName = decodeURI(blobName);
const normalizedBlobName = decodedBlobName.startsWith(
casefolderID + "/"
)
? decodedBlobName
: casefolderID +
(decodedBlobName.indexOf(".json") > 0 ? "/" : "/files/") +
decodedBlobName;
const downloaded = await downloadFile(
containerName,
bloblocation + decodeURI(blobName)
);
const downloaded = await downloadFile(
containerName,
normalizedBlobName
);
const responseFileName = decodedBlobName.split("/").pop();
res.setHeader(
"content-disposition",
"attachment; filename=" + decodeURI(blobName)
);
return res.status(200).send(downloaded);
res.setHeader(
"content-disposition",
"attachment; filename=" + responseFileName
);
return res.status(200).send(downloaded);
} catch (error) {
return respondError(res, {
status: 400,
code: "DOWNLOAD_BLOB_FAILED",
message: "Unable to download blob"
});
}
});
async function streamToBuffer(readableStream) {
return new Promise((resolve, reject) => {
const chunks = [];
readableStream.on("data", (data) => {
chunks.push(data instanceof Buffer ? data : Buffer.from(data));
});
readableStream.on("end", () => {
resolve(Buffer.concat(chunks));
});
readableStream.on("error", reject);
});
}
export const config = {
api: {
bodyParser: false
+97 -113
View File
@@ -73,12 +73,6 @@
*/
import {
createContainer,
getContainers,
getBlobs,
createRepBlob,
uploadFile,
uploadPDFAppealFiles,
downloadProgressFile,
getProgressBlobs,
getTempCaseBlob,
@@ -86,23 +80,9 @@ import {
} from "../../../actions/azurestorage";
import { hashAPIPath } from "../../../actions/core/hash";
import ReactPDF, {
Document,
Page,
Text,
View,
StyleSheet,
PDFViewer
} from "@react-pdf/renderer";
import middleware from "../middleware/middleware";
import nextConnect from "next-connect";
import fs from "fs";
import path from "path";
import ReactPDF from "@react-pdf/renderer";
import { getPickLists } from "../../../actions/services/referenceDataService";
import { planningappeals78_pdf } from "../../../components/pdftemplates/planningappeals78_pdf";
import { finalComments_pdf } from "../../../components/pdftemplates/finalComments_pdf";
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";
@@ -112,16 +92,15 @@ import { respondError, respondSuccess } from "../middleware/apiResponse";
// ApiProxy.post(async (req, res) => {
export default async function handler(req, res) {
var checkHash = req.query.hash;
var appealType = req.query.appealType;
var isDownload = req.query?.download === "true";
const checkHash = req.query.hash;
const appealType = req.query.appealType;
const isDownload = req.query?.download === "true";
let appealBodyObj =
typeof req.body === "string" ? JSON.parse(req.body) : req.body;
var containerID = appealBodyObj.containerID;
var casefolderID = appealBodyObj.casefolderID;
var caseRef = appealBodyObj.caseRef;
var checkquerypath = "/api/file/generateappealpdf";
const containerID = appealBodyObj.containerID;
const casefolderID = appealBodyObj.casefolderID;
let checkquerypath = "/api/file/generateappealpdf";
if (
typeof checkHash === "undefined" ||
@@ -184,92 +163,97 @@ export default async function handler(req, res) {
}
};
const tempCaseBlob = await getTempCaseBlob(containerID, casefolderID);
const blobProgress = await getProgressBlobs(containerID, casefolderID).then(
(data) => {
//console.log("Progress blob path:", data);
return downloadProgressFile(containerID, data.path, casefolderID);
}
);
try {
const tempCaseBlob = await getTempCaseBlob(containerID, casefolderID);
const progressBlob = await getProgressBlobs(containerID, casefolderID);
const blobProgress = await downloadProgressFile(
containerID,
progressBlob.path,
casefolderID
);
const uniqueArray = appealBodyObj.filesList.filter((obj, index, self) => {
// Check if the name has already been encountered
return index === self.findIndex((t) => t.name === obj.name);
});
Object.assign(blobProgress, {
"filesList": uniqueArray,
"caseObj": tempCaseBlob
});
const pickListData = await getPickLists(appealType);
// ReactPDF.render(
// <MyDocument docProps={blobProgress} pickListData={pickListData} />,
// process.cwd() + `/tmp/${blobProgress.pinswg_name}.pdf`
// );
// ReactPDF.renderToStream();
const streamToBuffer = async (readableStream) => {
return new Promise((resolve, reject) => {
const chunks = [];
readableStream.on("data", (chunk) => {
chunks.push(
Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
);
});
readableStream.on("end", () => resolve(Buffer.concat(chunks)));
readableStream.on("error", reject);
});
};
const renderedPDF = await ReactPDF.renderToStream(
<MyDocument docProps={blobProgress} pickListData={pickListData} />
);
const repDate = new Date();
let day = repDate.getDate();
let month = repDate.getMonth() + 1;
let year = repDate.getFullYear();
const pdfFileName =
year +
"-" +
("0" + month).slice(-2) +
"-" +
("0" + day).slice(-2) +
"_-_Appeal_Form";
const renderedPDFBuffer = await streamToBuffer(renderedPDF);
await createAppealPDFBlob(
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);
const uniqueArray = appealBodyObj.filesList.filter(
(obj, index, self) => {
return index === self.findIndex((t) => t.name === obj.name);
}
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"
});
);
Object.assign(blobProgress, {
"filesList": uniqueArray,
"caseObj": tempCaseBlob
});
const pickListData = await getPickLists(appealType);
// ReactPDF.render(
// <MyDocument docProps={blobProgress} pickListData={pickListData} />,
// process.cwd() + `/tmp/${blobProgress.pinswg_name}.pdf`
// );
// ReactPDF.renderToStream();
const streamToBuffer = async (readableStream) => {
return new Promise((resolve, reject) => {
const chunks = [];
readableStream.on("data", (chunk) => {
chunks.push(
Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
);
});
readableStream.on("end", () => resolve(Buffer.concat(chunks)));
readableStream.on("error", reject);
});
};
const renderedPDF = await ReactPDF.renderToStream(
MyDocument({
docProps: blobProgress,
pickListData: pickListData
})
);
const repDate = new Date();
let day = repDate.getDate();
let month = repDate.getMonth() + 1;
let year = repDate.getFullYear();
const pdfFileName =
year +
"-" +
("0" + month).slice(-2) +
"-" +
("0" + day).slice(-2) +
"_-_Appeal_Form";
const renderedPDFBuffer = await streamToBuffer(renderedPDF);
const data = await createAppealPDFBlob(
renderedPDFBuffer,
containerID,
blobProgress.pinswg_name || blobProgress.caseObj.ticketnumber
);
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 (error) {
return respondError(res, {
status: 400,
code: "GENERATE_APPEAL_PDF_FAILED",
message: "Failed to generate appeal PDF"
});
}
}
+64 -73
View File
@@ -1,45 +1,17 @@
import {
createContainer,
getContainers,
getBlobs,
createRepBlob,
uploadFile,
uploadPDFAppealFiles,
downloadProgressFile,
getProgressBlobs,
getTempCaseBlob,
createAppealPDFBlob
} from "../../../actions/azurestorage";
import {
getCaseByID,
getPortalModuleDetails,
getAppealPDFDocs
} from "../../../actions/services/caseService";
import { getFormCollectionByID } from "../../../components/utils";
import ReactPDF, {
Document,
Page,
Text,
View,
StyleSheet,
PDFViewer,
pdf
} from "@react-pdf/renderer";
import middleware from "../middleware/middleware";
import nextConnect from "next-connect";
import fs from "fs";
import path from "path";
import { pdf } from "@react-pdf/renderer";
import { getPickLists } from "../../../actions/services/referenceDataService";
import { planningappeals78_pdf } from "../../../components/pdftemplates/planningappeals78_pdf";
import { finalComments_pdf } from "../../../components/pdftemplates/finalComments_pdf";
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 } from "../middleware/apiResponse";
const getDetails = (resultsObj, detailsType) => {
const getDetails = (resultsObj) => {
if (!Array.isArray(resultsObj)) {
console.warn("getDetails: resultsObj is not an array");
return Promise.resolve([]);
}
@@ -51,7 +23,6 @@ const getDetails = (resultsObj, detailsType) => {
const form = getFormCollectionByID(appealType);
if (!form) {
console.warn("No form found for appeal type:", appealType);
return null;
}
@@ -64,54 +35,74 @@ const getDetails = (resultsObj, detailsType) => {
export default async function handler(req, res) {
const incidentid = req.body?.docProps;
if (!incidentid) return res.status(400).send("Missing incident ID");
const caseObj = await getCaseByID(incidentid);
if (!Array.isArray(caseObj) || caseObj.length === 0)
return res.status(404).send("Case not found");
const caseDetailsObj = await getDetails(caseObj, "myCases");
const fileList = (await getAppealPDFDocs(incidentid)) || [];
// Inject into the first item in `value` array
if (caseDetailsObj[0]?.value?.[0]) {
caseDetailsObj[0].value[0].pinswg_developmentdescription =
caseObj[0].description;
caseDetailsObj[0].value[0].caseObj = caseObj[0];
caseDetailsObj[0].value[0].filesList = fileList;
if (!incidentid) {
return respondError(res, {
status: 400,
code: "INCIDENT_ID_REQUIRED",
message: "Incident id is required"
});
}
const whichForm = getFormCollectionByID(caseObj[0].pinswg_appealcasetype);
const pickListData = await getPickLists(whichForm.UrlName);
try {
const caseObj = await getCaseByID(incidentid);
if (!Array.isArray(caseObj) || caseObj.length === 0) {
return respondError(res, {
status: 404,
code: "CASE_NOT_FOUND",
message: "Case not found"
});
}
const appealTypeMap = {
846040000: planningappeals78_pdf,
846040004: planningappeals78_pdf
};
const caseDetailsObj = await getDetails(caseObj);
const fileList = (await getAppealPDFDocs(incidentid)) || [];
//console.log(caseDetailsObj[0].value[0]);
if (caseDetailsObj[0]?.value?.[0]) {
caseDetailsObj[0].value[0].pinswg_developmentdescription =
caseObj[0].description;
caseDetailsObj[0].value[0].caseObj = caseObj[0];
caseDetailsObj[0].value[0].filesList = fileList;
}
const MyDocument = (values) => {
const type = caseObj[0].pinswg_appealcasetype;
const renderDocument = appealTypeMap[type] || other_pdf;
return renderDocument(values);
};
const whichForm = getFormCollectionByID(
caseObj[0].pinswg_appealcasetype
);
if (!whichForm?.UrlName) {
return respondError(res, {
status: 400,
code: "FORM_COLLECTION_NOT_FOUND",
message: "Unable to resolve form collection"
});
}
const pickListData = await getPickLists(whichForm.UrlName);
const pdfComponent = MyDocument({
docProps: caseDetailsObj[0].value[0],
pickListData
});
const appealTypeMap = {
846040000: planningappeals78_pdf,
846040004: planningappeals78_pdf
};
const pdfBuffer = await pdf(pdfComponent).toBuffer();
const renderDocument =
appealTypeMap[caseObj[0].pinswg_appealcasetype] || other_pdf;
const pdfComponent = renderDocument({
docProps: caseDetailsObj?.[0]?.value?.[0] || {},
pickListData
});
const repDate = new Date();
const formattedDate = repDate.toISOString().split("T")[0];
const pdfBuffer = await pdf(pdfComponent).toBuffer();
res.setHeader("Content-Type", "application/pdf");
res.setHeader(
"Content-Disposition",
`attachment; filename=${formattedDate}_-_Appeal_Form.pdf`
);
res.send(pdfBuffer);
const repDate = new Date();
const formattedDate = repDate.toISOString().split("T")[0];
res.setHeader("Content-Type", "application/pdf");
res.setHeader(
"Content-Disposition",
`attachment; filename=${formattedDate}_-_Appeal_Form.pdf`
);
return res.send(pdfBuffer);
} catch (error) {
return respondError(res, {
status: 400,
code: "APPEAL_PDF_COPY_GENERATION_FAILED",
message: "Unable to generate appeal PDF copy"
});
}
}
+39 -122
View File
@@ -72,28 +72,10 @@
* description: Success
*/
import {
createContainer,
getContainers,
getBlobs,
createRepBlob,
uploadFile,
uploadPDFRepFiles,
createRepPDFBlob
} from "../../../actions/azurestorage";
import { createRepPDFBlob } from "../../../actions/azurestorage";
import { hashAPIPath } from "../../../actions/core/hash";
import ReactPDF, {
Document,
Page,
Text,
View,
StyleSheet,
PDFViewer
} from "@react-pdf/renderer";
import middleware from "../middleware/middleware";
import nextConnect from "next-connect";
import fs from "fs";
import ReactPDF from "@react-pdf/renderer";
import { EnforcementQuestionnaire_pdf } from "../../../components/pdftemplates/enforcement_pdf";
import { lpaQuestionnaire_pdf } from "../../../components/pdftemplates/lpaQuestionnaire_pdf";
import { finalComments_pdf } from "../../../components/pdftemplates/finalComments_pdf";
@@ -109,7 +91,7 @@ import { respondError, respondSuccess } from "../middleware/apiResponse";
export default async function handler(req, res) {
const shouldDownload = req.query.download === "true";
var checkHash = req.query.hash;
const checkHash = req.query.hash;
let reqBodyobj =
typeof req.body === "string" ? JSON.parse(req.body) : req.body;
@@ -169,15 +151,11 @@ export default async function handler(req, res) {
// "\n//////////////////////\n"
// );
var containerID = reqBodyobj.containerID;
var casefolderID = reqBodyobj.casefolderID;
var caseRef = reqBodyobj.ticketnumber || reqBodyobj.caseRef;
var representationType = reqBodyobj.representationType;
var repRaiser = reqBodyobj.lastname;
var localeSelect = reqBodyobj.locale;
const containerID = reqBodyobj.containerID;
const caseRef = reqBodyobj.ticketnumber || reqBodyobj.caseRef;
//console.log("there are files:", Object.keys(req.files).length);
var checkquerypath = "/api/file/generatepdf";
let checkquerypath = "/api/file/generatepdf";
if (shouldDownload) {
checkquerypath += "?download=true";
@@ -247,67 +225,6 @@ export default async function handler(req, res) {
}
};
let repCapacity = "";
switch (reqBodyobj.representationCapacity) {
case "Appellant":
repCapacity = "APP";
break;
case "Agent":
repCapacity = "AGENT";
break;
case "Interested Party/Person":
repCapacity = "IP";
break;
case "Land Owner":
repCapacity = "LO";
break;
case "lpa":
case "LPA":
repCapacity = "LPA";
break;
default:
// code block
}
let repType = "";
switch (reqBodyobj.representationType) {
case "Statement":
repType = "Statement";
break;
case "Statement of common ground":
repType = "SCG";
break;
case "Written statement":
repType = "WS";
break;
case "Written statement of evidence":
repType = "WSE";
break;
case "Questionnaire":
repType = "Questionnaire";
break;
case "Final comments":
repType = "Comments";
break;
case "Local Impact Report":
repType = "Impact";
break;
case "Consultation Response":
repType = "Consultation_Response";
break;
case "Marine Impact Report":
repType = "Marine_Impact";
break;
case "Other":
repType = "OTHER";
break;
default:
// code block
}
//console.log(
// "///////////////////////////////////\n file created: \n" +
// process.cwd() +
@@ -336,40 +253,40 @@ export default async function handler(req, res) {
});
};
const renderedPDFStream = await ReactPDF.renderToStream(
<MyDocument docProps={reqBodyobj} />
);
const renderedPDFBuffer = await streamToBuffer(renderedPDFStream);
try {
const renderedPDFStream = await ReactPDF.renderToStream(
MyDocument({ docProps: reqBodyobj })
);
const renderedPDFBuffer = await streamToBuffer(renderedPDFStream);
await createRepPDFBlob(
renderedPDFBuffer,
containerID,
caseRef,
reqBodyobj.repfile_name
)
.then((data) => {
if (shouldDownload) {
const filename = `${reqBodyobj.repfile_name || "questionnaire"}.pdf`;
res.setHeader("Content-Type", "application/pdf");
res.setHeader(
"Content-Disposition",
`attachment; filename="${filename}"`
);
return res.status(200).send(renderedPDFBuffer);
}
const data = await createRepPDFBlob(
renderedPDFBuffer,
containerID,
caseRef,
reqBodyobj.repfile_name
);
return respondSuccess(res, {
status: "success",
data: data,
path: `${caseRef}/${reqBodyobj.repfile_name}/files/${reqBodyobj.repfile_name}.pdf`
});
})
.catch((error) => {
consoleLogger(error);
return respondError(res, {
status: 400,
code: "GENERATE_PDF_FAILED",
message: "Failed to generate PDF"
});
if (shouldDownload) {
const filename = `${reqBodyobj.repfile_name || "questionnaire"}.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: `${caseRef}/${reqBodyobj.repfile_name}/files/${reqBodyobj.repfile_name}.pdf`
});
} catch (error) {
consoleLogger(error);
return respondError(res, {
status: 400,
code: "GENERATE_PDF_FAILED",
message: "Failed to generate PDF"
});
}
}
+25 -12
View File
@@ -2,7 +2,6 @@ import {
downloadAllProgressFiles,
getAllProgressBlobs
} from "../../../actions/azurestorage";
import _ from "lodash";
import nextConnect from "next-connect";
import middleware from "../middleware/middleware";
import { hashAPIPath } from "../../../actions/core/hash";
@@ -12,8 +11,8 @@ const ApiProxy = nextConnect();
ApiProxy.use(middleware);
ApiProxy.get(async (req, res) => {
var containerName = req.query.container;
var checkHash = req.query.hash;
const containerName = req.query.container;
const checkHash = req.query.hash;
if (
typeof containerName === "undefined" ||
@@ -28,10 +27,17 @@ ApiProxy.get(async (req, res) => {
});
}
var checkquerypath =
"/api/file/getawaitingsubmissionfromblob?container=" + containerName;
const hashCandidatePaths = [
"/api/file/getawaitingsubmissionfromblob?container=" + containerName,
"/api/file/getawaitingsubmissionfromblob?container=" +
encodeURIComponent(containerName)
];
if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) {
const isHashValid = hashCandidatePaths.some(
(candidatePath) => hashAPIPath(candidatePath) == "&hash=" + checkHash
);
if (!isHashValid) {
return respondError(res, {
status: 400,
code: "INVALID_HASH",
@@ -39,13 +45,20 @@ ApiProxy.get(async (req, res) => {
});
}
const blobObj = await getAllProgressBlobs(containerName)
.then((data) => {
return downloadAllProgressFiles(containerName, data);
})
.then((data) => {
return respondSuccess(res, data);
try {
const progressBlobs = await getAllProgressBlobs(containerName);
const data = await downloadAllProgressFiles(
containerName,
progressBlobs
);
return respondSuccess(res, data);
} catch (error) {
return respondError(res, {
status: 400,
code: "GET_AWAITING_SUBMISSION_BLOB_FAILED",
message: "Failed to retrieve awaiting submission blobs"
});
}
});
export const config = {
@@ -11,7 +11,7 @@ const hasValue = (value) =>
typeof value === "string" && value.trim().length > 0;
export default async function ApiProxy(req, res) {
var containerName = req.query.container;
const containerName = req.query.container;
if (!hasValue(containerName)) {
return respondError(res, {
@@ -21,25 +21,23 @@ export default async function ApiProxy(req, res) {
});
}
var token = await getToken();
const token = await getToken();
var queryUrl =
const queryUrl =
"/api/file/getawaitingsubmissionfromblob?container=" + containerName;
return axios
.get(
try {
const { data } = await axios.get(
BASE_URL + queryUrl + hashAPIPath(queryUrl),
azureHeaders(token.access_token)
)
.then(({ data }) => {
return respondSuccess(res, data);
})
.catch((error) => {
consoleLogger(error);
return respondError(res, {
status: 400,
code: "GET_AWAITING_SUBMISSION_PROXY_FAILED",
message: "Failed to fetch awaiting submission blob"
});
);
return respondSuccess(res, data);
} catch (error) {
consoleLogger(error);
return respondError(res, {
status: 400,
code: "GET_AWAITING_SUBMISSION_PROXY_FAILED",
message: "Failed to fetch awaiting submission blob"
});
}
}
+37 -19
View File
@@ -40,10 +40,9 @@ const ApiProxy = nextConnect();
ApiProxy.use(middleware);
ApiProxy.get(async (req, res) => {
var containerName = req.query.container;
var casefolderID = req.query.casefolderID;
var checkHash = req.query.hash;
const containerName = req.query.container;
const casefolderID = req.query.casefolderID;
const checkHash = req.query.hash;
if (
typeof containerName === "undefined" ||
@@ -60,13 +59,24 @@ ApiProxy.get(async (req, res) => {
});
}
var checkquerypath =
"/api/file/getbloblist?container=" +
containerName +
"&casefolderID=" +
casefolderID;
const casefolderIDTrimmed = casefolderID.trim();
if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) {
const hashCandidatePaths = [
"/api/file/getbloblist?container=" +
containerName +
"&casefolderID=" +
casefolderIDTrimmed,
"/api/file/getbloblist?container=" +
containerName +
"&casefolderID=" +
encodeURIComponent(casefolderIDTrimmed)
];
const isHashValid = hashCandidatePaths.some(
(candidatePath) => hashAPIPath(candidatePath) == "&hash=" + checkHash
);
if (!isHashValid) {
return respondError(res, {
status: 400,
code: "INVALID_HASH",
@@ -74,16 +84,24 @@ ApiProxy.get(async (req, res) => {
});
}
const data =
casefolderID.split("/").length > 1
? await getRepsFilesBlobs(
containerName,
casefolderID.split("/")[0],
casefolderID.split("/")[1]
)
: await getBlobs(containerName, casefolderID);
try {
const data =
casefolderIDTrimmed.split("/").length > 1
? await getRepsFilesBlobs(
containerName,
casefolderIDTrimmed.split("/")[0],
casefolderIDTrimmed.split("/")[1]
)
: await getBlobs(containerName, casefolderIDTrimmed);
return respondSuccess(res, { "value": [data] });
return respondSuccess(res, { "value": [data] });
} catch (error) {
return respondError(res, {
status: 400,
code: "GET_BLOB_LIST_FAILED",
message: "Failed to retrieve blob list"
});
}
});
export const config = {
+17 -19
View File
@@ -11,8 +11,8 @@ const hasValue = (value) =>
typeof value === "string" && value.trim().length > 0;
export default async function ApiProxy(req, res) {
var containerName = req.query.container;
var casefolderID = req.query.casefolderID;
const containerName = req.query.container;
const casefolderID = req.query.casefolderID;
if (!hasValue(containerName) || !hasValue(casefolderID)) {
return respondError(res, {
@@ -22,28 +22,26 @@ export default async function ApiProxy(req, res) {
});
}
var token = await getToken();
const token = await getToken();
var queryUrl =
const queryUrl =
"/api/file/getbloblist?container=" +
containerName +
encodeURIComponent(containerName) +
"&casefolderID=" +
casefolderID;
encodeURIComponent(casefolderID);
return axios
.get(
try {
const { data } = await axios.get(
BASE_URL + queryUrl + hashAPIPath(queryUrl),
azureHeaders(token.access_token)
)
.then(({ data }) => {
return respondSuccess(res, data);
})
.catch((error) => {
consoleLogger(error);
return respondError(res, {
status: 400,
code: "GET_BLOB_LIST_PROXY_FAILED",
message: "Failed to fetch blob list"
});
);
return respondSuccess(res, data);
} catch (error) {
consoleLogger(error);
return respondError(res, {
status: 400,
code: "GET_BLOB_LIST_PROXY_FAILED",
message: "Failed to fetch blob list"
});
}
}
+37 -16
View File
@@ -12,10 +12,9 @@ const ApiProxy = nextConnect();
ApiProxy.use(middleware);
ApiProxy.get(async (req, res) => {
var containerName = req.query.container;
var casefolderID = req.query.casefolderID;
var checkHash = req.query.hash;
const containerName = req.query.container;
const casefolderID = req.query.casefolderID;
const checkHash = req.query.hash;
if (
typeof containerName === "undefined" ||
@@ -32,13 +31,24 @@ ApiProxy.get(async (req, res) => {
});
}
var checkquerypath =
"/api/file/getprogressobjblob?container=" +
containerName +
"&casefolderID=" +
casefolderID;
const casefolderIDTrimmed = casefolderID.trim();
if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) {
const hashCandidatePaths = [
"/api/file/getprogressobjblob?container=" +
containerName +
"&casefolderID=" +
casefolderIDTrimmed,
"/api/file/getprogressobjblob?container=" +
containerName +
"&casefolderID=" +
encodeURIComponent(casefolderIDTrimmed)
];
const isHashValid = hashCandidatePaths.some(
(candidatePath) => hashAPIPath(candidatePath) == "&hash=" + checkHash
);
if (!isHashValid) {
return respondError(res, {
status: 400,
code: "INVALID_HASH",
@@ -46,13 +56,24 @@ ApiProxy.get(async (req, res) => {
});
}
await getProgressBlobs(containerName, casefolderID)
.then((data) => {
return downloadProgressFile(containerName, data.path, casefolderID);
})
.then((data) => {
return respondSuccess(res, data);
try {
const progressBlob = await getProgressBlobs(
containerName,
casefolderIDTrimmed
);
const data = await downloadProgressFile(
containerName,
progressBlob.path,
casefolderIDTrimmed
);
return respondSuccess(res, data);
} catch (error) {
return respondError(res, {
status: 400,
code: "GET_PROGRESS_OBJ_BLOB_FAILED",
message: "Failed to retrieve progress blob"
});
}
});
export const config = {
+23 -23
View File
@@ -33,7 +33,6 @@ import {
downloadAllRepsFiles,
getRepsBlobs
} from "../../../actions/azurestorage";
import _ from "lodash";
import nextConnect from "next-connect";
import middleware from "../middleware/middleware";
import { hashAPIPath } from "../../../actions/core/hash";
@@ -44,10 +43,8 @@ const ApiProxy = nextConnect();
ApiProxy.use(middleware);
ApiProxy.get(async (req, res) => {
var containerName = req.query.container;
var casefolderID = req.query.casefolderID;
var checkHash = req.query.hash;
const containerName = req.query.container;
const checkHash = req.query.hash;
if (
typeof containerName === "undefined" ||
@@ -62,9 +59,16 @@ ApiProxy.get(async (req, res) => {
});
}
var checkquerypath = "/api/file/getrepsblob?container=" + containerName;
const hashCandidatePaths = [
"/api/file/getrepsblob?container=" + containerName,
"/api/file/getrepsblob?container=" + encodeURIComponent(containerName)
];
if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) {
const isHashValid = hashCandidatePaths.some(
(candidatePath) => hashAPIPath(candidatePath) == "&hash=" + checkHash
);
if (!isHashValid) {
return respondError(res, {
status: 400,
code: "INVALID_HASH",
@@ -72,23 +76,19 @@ ApiProxy.get(async (req, res) => {
});
}
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"
});
try {
const repsBlobs = await getRepsBlobs(containerName);
const result = await downloadAllRepsFiles(containerName, repsBlobs);
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;
+15 -16
View File
@@ -11,7 +11,7 @@ const hasValue = (value) =>
typeof value === "string" && value.trim().length > 0;
export default async function ApiProxy(req, res) {
var containerName = req.query.container;
const containerName = req.query.container;
if (!hasValue(containerName)) {
return respondError(res, {
@@ -21,24 +21,23 @@ export default async function ApiProxy(req, res) {
});
}
var token = await getToken();
const token = await getToken();
var queryUrl = "/api/file/getrepsblob?container=" + containerName;
const queryUrl =
"/api/file/getrepsblob?container=" + encodeURIComponent(containerName);
return axios
.get(
try {
const { data } = await axios.get(
BASE_URL + queryUrl + hashAPIPath(queryUrl),
azureHeaders(token.access_token)
)
.then(({ data }) => {
return respondSuccess(res, data);
})
.catch((error) => {
consoleLogger(error);
return respondError(res, {
status: 400,
code: "GET_REPS_BLOB_PROXY_FAILED",
message: "Failed to fetch representation blobs"
});
);
return respondSuccess(res, data);
} catch (error) {
consoleLogger(error);
return respondError(res, {
status: 400,
code: "GET_REPS_BLOB_PROXY_FAILED",
message: "Failed to fetch representation blobs"
});
}
}
+25 -22
View File
@@ -1,10 +1,4 @@
import {
createContainer,
createContainerSas,
getContainers,
getBlobs,
uploadFile
} from "../../../actions/azurestorage";
import { createContainer } from "../../../actions/azurestorage";
import { hashAPIPath } from "../../../actions/core/hash";
import { consoleLogger } from "../../../actions/core/logger";
import { respondError, respondSuccess } from "../middleware/apiResponse";
@@ -16,8 +10,8 @@ const ApiProxy = nextConnect();
ApiProxy.use(middleware);
ApiProxy.get(async (req, res) => {
var containerName = req.query.ident;
var checkHash = req.query.hash;
const containerName = req.query.ident;
const checkHash = req.query.hash;
if (
typeof containerName === "undefined" ||
@@ -32,9 +26,19 @@ ApiProxy.get(async (req, res) => {
});
}
var checkquerypath = "/api/file/setupcontainer?ident=" + containerName;
const containerNameTrimmed = containerName.trim();
if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) {
const hashCandidatePaths = [
"/api/file/setupcontainer?ident=" + containerNameTrimmed,
"/api/file/setupcontainer?ident=" +
encodeURIComponent(containerNameTrimmed)
];
const isHashValid = hashCandidatePaths.some(
(candidatePath) => hashAPIPath(candidatePath) == "&hash=" + checkHash
);
if (!isHashValid) {
return respondError(res, {
status: 400,
code: "INVALID_HASH",
@@ -42,18 +46,17 @@ ApiProxy.get(async (req, res) => {
});
}
await createContainer(containerName)
.then((data) => {
return respondSuccess(res, { data: "success", output: data });
})
.catch((error) => {
consoleLogger(error);
return respondError(res, {
status: 400,
code: "SETUP_CONTAINER_FAILED",
message: "Failed to setup container"
});
try {
const data = await createContainer(containerNameTrimmed);
return respondSuccess(res, { data: "success", output: data });
} catch (error) {
consoleLogger(error);
return respondError(res, {
status: 400,
code: "SETUP_CONTAINER_FAILED",
message: "Failed to setup container"
});
}
});
export const config = {
+17 -19
View File
@@ -1,5 +1,4 @@
import axios from "axios";
import CryptoJS from "crypto-js";
import { consoleLogger } from "../../../actions/core/logger";
import { getToken } from "../../../actions/core/token";
import { hashAPIPath } from "../../../actions/core/hash";
@@ -10,12 +9,12 @@ const WEBAPI_URL =
"https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/";
export default async function ApiProxy(req, res) {
var data = JSON.stringify(req.body);
var appealObj = req.query.appealObj;
var incidentID = req.query.incident;
const data = JSON.stringify(req.body);
const appealObj = req.query.appealObj;
const incidentID = req.query.incident;
var updateFormCollection = req.query.updateFormCollection;
var queryUrl = updateFormCollection + "(" + appealObj + ")";
const updateFormCollection = req.query.updateFormCollection;
const queryUrl = updateFormCollection + "(" + appealObj + ")";
if (
typeof updateFormCollection === "undefined" ||
@@ -35,9 +34,9 @@ export default async function ApiProxy(req, res) {
});
}
var token = await getToken();
const token = await getToken();
var config = {
const config = {
method: "patch",
url: WEBAPI_URL + queryUrl + hashAPIPath(queryUrl),
headers: {
@@ -53,16 +52,15 @@ export default async function ApiProxy(req, res) {
//console.log("update case:", data);
return axios(config)
.then(({ data }) => {
return respondSuccess(res, data);
})
.catch((error) => {
consoleLogger(error);
return respondError(res, {
status: 400,
code: "UPDATE_CASE_FAILED",
message: "Failed to update case"
});
try {
const { data: responseData } = await axios(config);
return respondSuccess(res, responseData);
} catch (error) {
consoleLogger(error);
return respondError(res, {
status: 400,
code: "UPDATE_CASE_FAILED",
message: "Failed to update case"
});
}
}
+18 -21
View File
@@ -1,8 +1,4 @@
import {
createBlob,
createRepBlob,
uploadFile
} from "../../../actions/azurestorage";
import { createBlob, createRepBlob } from "../../../actions/azurestorage";
import { hashAPIPath } from "../../../actions/core/hash";
import { respondError, respondSuccess } from "../middleware/apiResponse";
@@ -13,7 +9,7 @@ const ApiProxy = nextConnect();
ApiProxy.use(middleware);
ApiProxy.post(async (req, res) => {
var checkHash = req.query.hash;
const checkHash = req.query.hash;
if (typeof checkHash === "undefined" || checkHash.length === 0) {
return respondError(res, {
@@ -23,9 +19,12 @@ ApiProxy.post(async (req, res) => {
});
}
var checkquerypath = "/api/file/upload";
const hashCandidatePaths = ["/api/file/upload", "/api/file/upload?"];
const isHashValid = hashCandidatePaths.some(
(candidatePath) => hashAPIPath(candidatePath) == "?hash=" + checkHash
);
if (hashAPIPath(checkquerypath) != "?hash=" + checkHash) {
if (!isHashValid) {
return respondError(res, {
status: 400,
code: "INVALID_HASH",
@@ -51,21 +50,19 @@ ApiProxy.post(async (req, res) => {
});
}
return (
repOrAppeal
try {
const data = await (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"
});
: createBlob(appealData, containerID, casefolderID));
return respondSuccess(res, { data });
} catch {
return respondError(res, {
status: 400,
code: "UPLOAD_FAILED",
message: "Failed to upload blob"
});
}
});
export const config = {
+12 -10
View File
@@ -1,8 +1,4 @@
import {
createBlob,
createRepBlob,
uploadSingleFile
} from "../../../actions/azurestorage";
import { uploadSingleFile } from "../../../actions/azurestorage";
import nextConnect from "next-connect";
import middleware from "../middleware/middleware";
@@ -37,7 +33,7 @@ const ApiProxy = nextConnect();
ApiProxy.use(middleware);
ApiProxy.post(async (req, res) => {
var checkHash = req.query.hash;
const checkHash = req.query.hash;
if (typeof checkHash === "undefined" || checkHash.length === 0) {
return respondError(res, {
@@ -47,9 +43,15 @@ ApiProxy.post(async (req, res) => {
});
}
var checkquerypath = "/api/file/uploadsinglefile";
const hashCandidatePaths = [
"/api/file/uploadsinglefile",
"/api/file/uploadsinglefile?"
];
const isHashValid = hashCandidatePaths.some(
(candidatePath) => hashAPIPath(candidatePath) == "?hash=" + checkHash
);
if (hashAPIPath(checkquerypath) != "?hash=" + checkHash) {
if (!isHashValid) {
return respondError(res, {
status: 400,
code: "INVALID_HASH",
@@ -87,8 +89,8 @@ ApiProxy.post(async (req, res) => {
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
];
var allowedFilesFormData = {};
var invalidFiles = []; // To store the names of invalid files
const allowedFilesFormData = {};
const invalidFiles = []; // To store the names of invalid files
for (const [fileName, fileDetails] of Object.entries(uploadedFiles)) {
const file = fileDetails[0];
@@ -2,12 +2,14 @@ const runHelperTests = require("./api-response-helper.test.cjs");
const runHandlerTests = require("./file-handler-contract.test.cjs");
const runEmailHandlerTests = require("./email-handler-contract.test.cjs");
const runEndpointHandlerTests = require("./endpoint-handler-contract.test.cjs");
const runDocumentsHandlerTests = require("./documents-handler-contract.test.cjs");
const run = async () => {
await runHelperTests();
await runHandlerTests();
await runEmailHandlerTests();
await runEndpointHandlerTests();
await runDocumentsHandlerTests();
console.log("Phase 21 combined suite passed.");
};
@@ -0,0 +1,134 @@
const assert = require("assert");
const { Readable } = require("stream");
const { loadModule } = require("./_shared.cjs");
const tests = [];
const test = (name, fn) => tests.push({ name, fn });
const createRedirectRes = () => {
const state = {
redirectedTo: null,
headers: {},
piped: false,
headersSent: false
};
return {
state,
get headersSent() {
return state.headersSent;
},
setHeader(key, value) {
state.headers[key.toLowerCase()] = value;
},
redirect(path) {
state.redirectedTo = path;
state.headersSent = true;
return path;
}
};
};
test("documents download redirects when id/hash query is missing", async () => {
const mod = loadModule("pages/api/documents/download/[id].js", {
getToken: async () => ({ access_token: "tok" }),
consoleLogger: () => {},
axios: { get: async () => ({}) }
});
const req = { query: { id: "DOC-1" } };
const res = createRedirectRes();
await mod.default(req, res);
assert.strictEqual(res.state.redirectedTo, "/filenotavailable");
});
test("documents download success sets attachment headers and pipes stream", async () => {
const stream = new Readable({
read() {}
});
const mod = loadModule("pages/api/documents/download/[id].js", {
setTimeout: (fn) => {
fn();
return 0;
},
getToken: async () => ({ access_token: "tok" }),
consoleLogger: () => {},
axios: {
get: async () => ({
headers: {
"content-disposition": "attachment; filename=test-file.pdf"
},
data: stream
})
}
});
const req = { query: { id: "DOC-1", hash: "h1" } };
const res = createRedirectRes();
stream.pipe = (target) => {
target.state.piped = true;
target.state.headersSent = true;
return target;
};
await mod.default(req, res);
assert.strictEqual(
res.state.headers["content-disposition"],
"attachment; filename=test-file.pdf"
);
assert.strictEqual(
res.state.headers["content-type"],
"application/octet-stream"
);
assert.strictEqual(res.state.piped, true);
assert.strictEqual(res.state.redirectedTo, null);
});
test("documents download redirects when relay request fails", async () => {
const mod = loadModule("pages/api/documents/download/[id].js", {
setTimeout: (fn) => {
fn();
return 0;
},
getToken: async () => ({ access_token: "tok" }),
consoleLogger: () => {},
axios: {
get: async () => {
throw new Error("relay failed");
}
}
});
const req = { query: { id: "DOC-1", hash: "h1" } };
const res = createRedirectRes();
await mod.default(req, res);
assert.strictEqual(res.state.redirectedTo, "/filenotavailable");
});
const run = async () => {
let passed = 0;
for (const currentTest of tests) {
await currentTest.fn();
passed += 1;
}
console.log(
`Phase 21 documents-handler contract tests passed (${passed}/${tests.length}).`
);
};
module.exports = run;
if (require.main === module) {
run().catch((error) => {
console.error(error);
process.exit(1);
});
}
@@ -323,6 +323,93 @@ test("getportallogin catch path returns PORTAL_LOGIN_FETCH_FAILED", async () =>
);
});
test("getportallogin accepts encoded email hash variant", async () => {
const mod = loadModule("pages/api/endpoint/getportallogin_api.js", {
respondError: respondErrorMock,
respondSuccess: respondSuccessMock,
getToken: async () => ({ access_token: "token" }),
hashAPIPath: (input) =>
input && input.includes("emailAddress=user%2Btest%40local")
? "&hash=expected"
: "&hash=other",
azureHeadersPaged: () => ({}),
axios: {
get: async () => ({ data: { value: [{ contactid: "c1" }] } })
},
consoleLogger: () => {}
});
const req = {
query: { emailAddress: "user+test@local", hash: "expected" }
};
const res = createRes();
await mod.default(req, res);
assert.strictEqual(res.state.statusCode, 200);
assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), {
value: [{ contactid: "c1" }]
});
});
test("getnewappeals catch path returns ADMIN_NEW_APPEALS_FETCH_FAILED", async () => {
const mod = loadModule("pages/api/admin/getnewappeals_api.js", {
respondError: respondErrorMock,
respondSuccess: respondSuccessMock,
getToken: async () => ({ access_token: "token" }),
hashAPIPath: () => "&hash=expected",
azureHeadersPagedCustom: () => ({}),
axios: {
get: async () => {
throw new Error("admin new appeals failed");
}
},
consoleLogger: () => {},
_: { has: () => false }
});
const req = { query: {} };
const res = createRes();
await mod.default(req, res);
assert.strictEqual(res.state.statusCode, 400);
assert.strictEqual(
res.state.jsonBody.error.code,
"ADMIN_NEW_APPEALS_FETCH_FAILED"
);
});
test("getlatestdocuments catch path returns ADMIN_LATEST_DOCS_FETCH_FAILED", async () => {
const mod = loadModule("pages/api/admin/getlatestdocuments_api.js", {
respondError: respondErrorMock,
respondSuccess: respondSuccessMock,
getToken: async () => ({ access_token: "token" }),
hashAPIPath: () => "&hash=expected",
azureHeadersPagedCustom: () => ({}),
axios: {
get: async () => {
throw new Error("latest docs failed");
}
},
consoleLogger: () => {},
process: { env: { HASHKEY: "abc" } },
CryptoJS: {
HmacSHA256: () => ({ toString: () => "hash" }),
enc: { Hex: { parse: () => "parsed" } }
},
_: { has: () => false }
});
const req = { query: {} };
const res = createRes();
await mod.default(req, res);
assert.strictEqual(res.state.statusCode, 400);
assert.strictEqual(
res.state.jsonBody.error.code,
"ADMIN_LATEST_DOCS_FETCH_FAILED"
);
});
test("getportalloginproxy catch path returns PORTAL_LOGIN_PROXY_FETCH_FAILED", async () => {
const mod = loadModule("pages/api/endpoint/getportalloginproxy_api.js", {
respondError: respondErrorMock,
@@ -3688,6 +3775,37 @@ test("getsipsmedia returns success contract", async () => {
assert.strictEqual(res.state.jsonBody["@odata.count"], 4);
});
test("getsipsmedia returns METHOD_NOT_ALLOWED for non-GET methods", async () => {
const mod = loadModule("pages/api/endpoint/getsipsmedia_api.js", {
respondSuccess: respondSuccessMock,
respondError: respondErrorMock
});
const req = { method: "POST", query: {} };
const res = createRes();
await mod.default(req, res);
assert.strictEqual(res.state.statusCode, 405);
assert.strictEqual(res.state.jsonBody.error.code, "METHOD_NOT_ALLOWED");
});
test("getappealtypesfornewappeal returns METHOD_NOT_ALLOWED for non-GET methods", async () => {
const mod = loadModule(
"pages/api/endpoint/getappealtypesfornewappeal_api.js",
{
respondSuccess: respondSuccessMock,
respondError: respondErrorMock
}
);
const req = { method: "POST", query: {} };
const res = createRes();
await mod.default(req, res);
assert.strictEqual(res.state.statusCode, 405);
assert.strictEqual(res.state.jsonBody.error.code, "METHOD_NOT_ALLOWED");
});
const run = async () => {
let passed = 0;
for (const currentTest of tests) {
File diff suppressed because it is too large Load Diff