@gov-cy/govcy-express-services 1.0.0-alpha.18 → 1.0.0-alpha.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -326,6 +326,13 @@ Here is an example JSON config:
|
|
|
326
326
|
"serviceId": "TEST_SUBMISSION_API_SERVIVE_ID",
|
|
327
327
|
"dsfgtwApiKey": "TEST_SUBMISSION_DSF_GTW_KEY"
|
|
328
328
|
},
|
|
329
|
+
"fileDeleteAPIEndpoint": { //<-- File delete API endpoint
|
|
330
|
+
"url": "TEST_DELETE_FILE_API_URL",
|
|
331
|
+
"method": "DELETE",
|
|
332
|
+
"clientKey": "TEST_SUBMISSION_API_CLIENT_KEY",
|
|
333
|
+
"serviceId": "TEST_SUBMISSION_API_SERVIVE_ID",
|
|
334
|
+
"dsfgtwApiKey": "TEST_SUBMISSION_DSF_GTW_KEY"
|
|
335
|
+
},
|
|
329
336
|
"eligibilityAPIEndpoints": [ //<-- Eligibility API endpoints
|
|
330
337
|
{
|
|
331
338
|
"url": "TEST_ELIGIBILITY_2_API_URL",
|
|
@@ -696,6 +703,7 @@ Here are some details explaining the JSON structure:
|
|
|
696
703
|
- `submissionPutAPIEndpoint`: The submission put API endpoint, to be used for temporary saving the submission data. See more on the [temporary save feature](#-temporary-save-feature) section below.
|
|
697
704
|
- `fileUploadAPIEndpoint`: The file upload API endpoint, to be used for uploading files. See more on the [file upload feature](#%EF%B8%8F-files-uploads-feature) section below.
|
|
698
705
|
- `fileDownloadAPIEndpoint`: The file download API endpoint, to be used for downloading files. See more on the [file upload feature](#%EF%B8%8F-files-uploads-feature) section below.
|
|
706
|
+
- `fileDeleteAPIEndpoint`: The file delete API endpoint, to be used for deleting files. See more on the [file upload feature](#%EF%B8%8F-files-uploads-feature) section below.
|
|
699
707
|
- `pages` array: An array of page objects, each representing a page in the site.
|
|
700
708
|
- `pageData` object: Contains the metadata to be rendered on the page. See [govcy-frontend-renderer](https://github.com/gov-cy/govcy-frontend-renderer/tree/main#site-and-page-meta-data-explained) for more details
|
|
701
709
|
- `nextPage`: The URL of the next page to be rendered after the user clicks the `continue` button.
|
|
@@ -2168,7 +2176,7 @@ The [💾 Temporary save feature](#-temporary-save-feature) must be enabled for
|
|
|
2168
2176
|
|
|
2169
2177
|
|
|
2170
2178
|
#### How to enable and configure file uploads
|
|
2171
|
-
To use this feature, configure the config JSON file. In your service’s `site` object, add
|
|
2179
|
+
To use this feature, configure the config JSON file. In your service’s `site` object, add a `fileUploadAPIEndpoint`, `fileDownloadAPIEndpoint`and `fileDeleteAPIEndpoint` entry:
|
|
2172
2180
|
|
|
2173
2181
|
```json
|
|
2174
2182
|
"fileUploadAPIEndpoint": {
|
|
@@ -2182,6 +2190,12 @@ To use this feature, configure the config JSON file. In your service’s `site`
|
|
|
2182
2190
|
"method": "GET",
|
|
2183
2191
|
"clientKey": "TEST_SUBMISSION_API_CLIENT_KEY",
|
|
2184
2192
|
"serviceId": "TEST_SUBMISSION_API_SERVIVE_ID"
|
|
2193
|
+
},
|
|
2194
|
+
"fileDeleteAPIEndpoint": {
|
|
2195
|
+
"url": "TEST_DELETE_FILE_API_URL",
|
|
2196
|
+
"method": "DELETE",
|
|
2197
|
+
"clientKey": "TEST_SUBMISSION_API_CLIENT_KEY",
|
|
2198
|
+
"serviceId": "TEST_SUBMISSION_API_SERVIVE_ID"
|
|
2185
2199
|
}
|
|
2186
2200
|
```
|
|
2187
2201
|
|
|
@@ -2301,7 +2315,10 @@ Here is a sample code section of a page definition with a file input field:
|
|
|
2301
2315
|
- **When clinking `view`**:
|
|
2302
2316
|
- The file is downloaded using the `fileDownloadAPIEndpoint` and opened in a new tab.
|
|
2303
2317
|
- **When clinking `delete`**:
|
|
2304
|
-
- A confimation page is displayed asking the user to confirm the deletion. If the user confirms
|
|
2318
|
+
- A confimation page is displayed asking the user to confirm the deletion. If the user confirms (clicks `Yes`):
|
|
2319
|
+
- The file is deleted using the `fileDeleteAPIEndpoint`
|
|
2320
|
+
- The file is deleted from the data layer and the `fileView` element is removed from the page.
|
|
2321
|
+
- If the same file is used on another page (with the same `fileId` and `sha256`), they are also removed from the data layer
|
|
2305
2322
|
- **On the `review` page after upload**:
|
|
2306
2323
|
- The element is displayed with a link to `View file`. A `Change` link is also displayed for the whole page.
|
|
2307
2324
|
- **On the `success` page and email after upload**:
|
|
@@ -2441,6 +2458,65 @@ HTTP/1.1 404 Not Found
|
|
|
2441
2458
|
}
|
|
2442
2459
|
```
|
|
2443
2460
|
|
|
2461
|
+
#### `fileDeleteAPIEndpoint` `DELETE` API Request and Response
|
|
2462
|
+
This API is used to delete the file uploaded by the user. It returns the file data in Base64.
|
|
2463
|
+
> ⚠️ **Important note:**
|
|
2464
|
+
> If the same file (same `fileId` and `sha256`) is used for other fields in the same application for the same service and the same user, when deleted it will be removed from all instances in the data layer. A warning will appear in the user's delete confirmation page to warn the users in such cases.
|
|
2465
|
+
|
|
2466
|
+
**Request:**
|
|
2467
|
+
|
|
2468
|
+
- **HTTP Method**: DELETE
|
|
2469
|
+
- **URL**: Resolved from the url property in your config (from the environment variable) concatenated with `/:fileid/:sha256`. For example `https://example.com/api/fileDelete/123456789123456/12345678901234567890123`:
|
|
2470
|
+
- `fileid` is the `fileId` of the file uploaded by the user.
|
|
2471
|
+
- `sha256` is the `sha256` of the file uploaded by the user.
|
|
2472
|
+
- **Headers**:
|
|
2473
|
+
- **Authorization**: `Bearer <access_token>` (form user's cyLogin access token)
|
|
2474
|
+
- **client-key**: `<clientKey>` (from config/env)
|
|
2475
|
+
- **service-id**: `<serviceId>` (from config/env)
|
|
2476
|
+
- **Accept**: `text/plain`
|
|
2477
|
+
|
|
2478
|
+
**Example Request:**
|
|
2479
|
+
|
|
2480
|
+
```http
|
|
2481
|
+
DELETE fileDelete/123456789123456/12345678901234567890123 HTTP/1.1
|
|
2482
|
+
Host: localhost:3002
|
|
2483
|
+
Authorization: Bearer eyJhbGciOi...
|
|
2484
|
+
client-key: 12345678901234567890123456789000
|
|
2485
|
+
service-id: 123
|
|
2486
|
+
Accept: text/plain
|
|
2487
|
+
Content-Type: application/json
|
|
2488
|
+
```
|
|
2489
|
+
|
|
2490
|
+
**Response:**
|
|
2491
|
+
|
|
2492
|
+
The API is expected to return a JSON response with the following structure:
|
|
2493
|
+
|
|
2494
|
+
**When file is found and deleted:**
|
|
2495
|
+
|
|
2496
|
+
```http
|
|
2497
|
+
HTTP/1.1 200 OK
|
|
2498
|
+
|
|
2499
|
+
{
|
|
2500
|
+
"ErrorCode": 0,
|
|
2501
|
+
"ErrorMessage": null,
|
|
2502
|
+
"Succeeded": true
|
|
2503
|
+
}
|
|
2504
|
+
```
|
|
2505
|
+
|
|
2506
|
+
**When file is NOT found:**
|
|
2507
|
+
|
|
2508
|
+
```http
|
|
2509
|
+
HTTP/1.1 404 Not Found
|
|
2510
|
+
|
|
2511
|
+
{
|
|
2512
|
+
"ErrorCode": 404,
|
|
2513
|
+
"ErrorMessage": "File not found",
|
|
2514
|
+
"InformationMessage": null,
|
|
2515
|
+
"Data": null,
|
|
2516
|
+
"Succeeded": false
|
|
2517
|
+
}
|
|
2518
|
+
```
|
|
2519
|
+
|
|
2444
2520
|
#### File uploads in the data layer
|
|
2445
2521
|
The system uses the `fileId` and `sha256` to identify the file uploaded by the user. The file information are stored in the data layer in the following format:
|
|
2446
2522
|
|
|
@@ -2566,6 +2642,7 @@ TEST_SUBMISSION_PUT_API_URL=http://localhost:3002/save
|
|
|
2566
2642
|
# Optional File Upload and download endpoints (test service)
|
|
2567
2643
|
TEST_FILE_UPLOAD_API_URL=http://localhost:3002/fileUpload
|
|
2568
2644
|
TEST_FILE_DOWNLOAD_API_URL=http://localhost:3002/fileDownload
|
|
2645
|
+
TEST_FILE_DELETE_API_URL=http://localhost:3002/fileDelete
|
|
2569
2646
|
|
|
2570
2647
|
# Eligibility checks (optional test APIs)
|
|
2571
2648
|
TEST_ELIGIBILITY_1_API_URL=http://localhost:3002/eligibility1
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gov-cy/govcy-express-services",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.19",
|
|
4
4
|
"description": "An Express-based system that dynamically renders services using @gov-cy/govcy-frontend-renderer and posts data to a submission API.",
|
|
5
5
|
"author": "DMRID - DSF Team",
|
|
6
6
|
"license": "MIT",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"@gov-cy/dsf-email-templates": "^2.1.0",
|
|
54
|
-
"@gov-cy/govcy-frontend-renderer": "^1.
|
|
54
|
+
"@gov-cy/govcy-frontend-renderer": "^1.23.0",
|
|
55
55
|
"axios": "^1.9.0",
|
|
56
56
|
"cookie-parser": "^1.4.7",
|
|
57
57
|
"dotenv": "^16.3.1",
|
|
@@ -2,10 +2,11 @@ import * as govcyResources from "../resources/govcyResources.mjs";
|
|
|
2
2
|
import * as dataLayer from "../utils/govcyDataLayer.mjs";
|
|
3
3
|
import { logger } from "../utils/govcyLogger.mjs";
|
|
4
4
|
import { pageContainsFileInput } from "../utils/govcyHandleFiles.mjs";
|
|
5
|
-
import { whatsIsMyEnvironment } from '../utils/govcyEnvVariables.mjs';
|
|
5
|
+
import { whatsIsMyEnvironment, getEnvVariable, getEnvVariableBool } from '../utils/govcyEnvVariables.mjs';
|
|
6
6
|
import { handleMiddlewareError } from "../utils/govcyUtils.mjs";
|
|
7
7
|
import { getPageConfigData } from "../utils/govcyLoadConfigData.mjs";
|
|
8
8
|
import { evaluatePageConditions } from "../utils/govcyExpressions.mjs";
|
|
9
|
+
import { govcyApiRequest } from "../utils/govcyApiRequest.mjs";
|
|
9
10
|
import { URL } from "url";
|
|
10
11
|
|
|
11
12
|
|
|
@@ -50,7 +51,7 @@ export function govcyFileDeletePageHandler() {
|
|
|
50
51
|
const elementData = dataLayer.getFormDataValue(req.session, siteId, pageUrl, elementName)
|
|
51
52
|
|
|
52
53
|
// If the element data is not found, return an error response
|
|
53
|
-
if (!elementData) {
|
|
54
|
+
if (!elementData || !elementData?.sha256 || !elementData?.fileId) {
|
|
54
55
|
return handleMiddlewareError(`File input [${elementName}] data not found on this page`, 404, next);
|
|
55
56
|
}
|
|
56
57
|
|
|
@@ -80,6 +81,20 @@ export function govcyFileDeletePageHandler() {
|
|
|
80
81
|
]
|
|
81
82
|
};
|
|
82
83
|
|
|
84
|
+
//contruct the warning if the file was uploaded more than once
|
|
85
|
+
const warningSameFile = {
|
|
86
|
+
element: "warning",
|
|
87
|
+
params: {
|
|
88
|
+
text: govcyResources.staticResources.text.deleteSameFileWarning,
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const showSameFileWarning = dataLayer.isFileUsedInSiteInputDataAgain(
|
|
93
|
+
req.session,
|
|
94
|
+
siteId,
|
|
95
|
+
elementData
|
|
96
|
+
);
|
|
97
|
+
|
|
83
98
|
// Construct page title
|
|
84
99
|
const pageRadios = {
|
|
85
100
|
element: "radios",
|
|
@@ -88,7 +103,8 @@ export function govcyFileDeletePageHandler() {
|
|
|
88
103
|
name: "deleteFile",
|
|
89
104
|
legend: pageTitle,
|
|
90
105
|
isPageHeading: true,
|
|
91
|
-
classes: "govcy-mb-6"
|
|
106
|
+
classes: "govcy-mb-6",// only include the warning block when the file is referenced >1 times
|
|
107
|
+
elements: showSameFileWarning ? [warningSameFile] : [],
|
|
92
108
|
items: [
|
|
93
109
|
{
|
|
94
110
|
value: "yes",
|
|
@@ -171,7 +187,7 @@ export function govcyFileDeletePageHandler() {
|
|
|
171
187
|
* This middleware processes the post, validates the form and handles the file data layer
|
|
172
188
|
*/
|
|
173
189
|
export function govcyFileDeletePostHandler() {
|
|
174
|
-
return (req, res, next) => {
|
|
190
|
+
return async (req, res, next) => {
|
|
175
191
|
try {
|
|
176
192
|
// Extract siteId and pageUrl from request
|
|
177
193
|
let { siteId, pageUrl, elementName } = req.params;
|
|
@@ -198,6 +214,15 @@ export function govcyFileDeletePostHandler() {
|
|
|
198
214
|
if (!fileInputElement) {
|
|
199
215
|
return handleMiddlewareError(`File input [${elementName}] not allowed on this page`, 404, next);
|
|
200
216
|
}
|
|
217
|
+
|
|
218
|
+
//get element data
|
|
219
|
+
const elementData = dataLayer.getFormDataValue(req.session, siteId, pageUrl, elementName)
|
|
220
|
+
|
|
221
|
+
// If the element data is not found, return an error response
|
|
222
|
+
if (!elementData || !elementData?.sha256 || !elementData?.fileId) {
|
|
223
|
+
return handleMiddlewareError(`File input [${elementName}] data not found on this page`, 404, next);
|
|
224
|
+
}
|
|
225
|
+
|
|
201
226
|
// the page base return url
|
|
202
227
|
const pageBaseReturnUrl = `http://localhost:3000/${siteId}/${pageUrl}`;
|
|
203
228
|
|
|
@@ -220,13 +245,62 @@ export function govcyFileDeletePostHandler() {
|
|
|
220
245
|
|
|
221
246
|
//if no validation errors
|
|
222
247
|
if (req.body.deleteFile === "yes") {
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
248
|
+
// Try to delete the file via the delete API.
|
|
249
|
+
// If it fails, log the error but continue to remove the file from the session
|
|
250
|
+
try {
|
|
251
|
+
// Get the delete file configuration
|
|
252
|
+
const deleteCfg = serviceCopy?.site?.fileDeleteAPIEndpoint;
|
|
253
|
+
// Check if download file configuration is available
|
|
254
|
+
if (!deleteCfg?.url || !deleteCfg?.clientKey || !deleteCfg?.serviceId) {
|
|
255
|
+
return handleMiddlewareError(`File delete APU configuration not found`, 404, next);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Environment vars
|
|
259
|
+
const allowSelfSignedCerts = getEnvVariableBool("ALLOW_SELF_SIGNED_CERTIFICATES", false);
|
|
260
|
+
let url = getEnvVariable(deleteCfg.url || "", false);
|
|
261
|
+
const clientKey = getEnvVariable(deleteCfg.clientKey || "", false);
|
|
262
|
+
const serviceId = getEnvVariable(deleteCfg.serviceId || "", false);
|
|
263
|
+
const dsfGtwKey = getEnvVariable(deleteCfg?.dsfgtwApiKey || "", "");
|
|
264
|
+
const method = (deleteCfg?.method || "GET").toLowerCase();
|
|
265
|
+
|
|
266
|
+
// Check if the upload API is configured correctly
|
|
267
|
+
if (!url || !clientKey) {
|
|
268
|
+
return handleMiddlewareError(`Missing environment variables for upload`, 404, next);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Construct the URL with tag being the elementName
|
|
272
|
+
url += `/${encodeURIComponent(elementData.fileId)}/${encodeURIComponent(elementData.sha256)}`;
|
|
273
|
+
|
|
274
|
+
// Get the user
|
|
275
|
+
const user = dataLayer.getUser(req.session);
|
|
276
|
+
// Perform the delete request
|
|
277
|
+
const response = await govcyApiRequest(
|
|
278
|
+
method,
|
|
279
|
+
url,
|
|
280
|
+
{},
|
|
281
|
+
true,
|
|
282
|
+
user,
|
|
283
|
+
{
|
|
284
|
+
accept: "text/plain",
|
|
285
|
+
"client-key": clientKey,
|
|
286
|
+
"service-id": serviceId,
|
|
287
|
+
...(dsfGtwKey !== "" && { "dsfgtw-api-key": dsfGtwKey })
|
|
288
|
+
},
|
|
289
|
+
3,
|
|
290
|
+
allowSelfSignedCerts
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
// If not succeeded, handle error
|
|
294
|
+
if (!response?.Succeeded) {
|
|
295
|
+
logger.error("fileDeleteAPIEndpoint returned succeeded false");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
} catch (error) {
|
|
299
|
+
logger.error(`fileDeleteAPIEndpoint Call failed: ${error.message}`);
|
|
300
|
+
}
|
|
228
301
|
// if succeeded all good
|
|
229
|
-
dataLayer.storePageDataElement(req.session, siteId, pageUrl, elementName, "");
|
|
302
|
+
// dataLayer.storePageDataElement(req.session, siteId, pageUrl, elementName, "");
|
|
303
|
+
dataLayer.removeAllFilesFromSite(req.session, siteId, { fileId: elementData.fileId, sha256: elementData.sha256 });
|
|
230
304
|
logger.info(`File deleted by user`, { siteId, pageUrl, elementName });
|
|
231
305
|
}
|
|
232
306
|
// construct the page url
|
|
@@ -239,6 +313,7 @@ export function govcyFileDeletePostHandler() {
|
|
|
239
313
|
// redirect to the page (relative path)
|
|
240
314
|
res.redirect(myUrl.pathname + myUrl.search);
|
|
241
315
|
} catch (error) {
|
|
316
|
+
logger.error("Error in govcyFileDeletePostHandler middleware:", error.message);
|
|
242
317
|
return next(error); // Pass error to govcyHttpErrorHandler
|
|
243
318
|
}
|
|
244
319
|
};
|
|
@@ -123,7 +123,7 @@ export const staticResources = {
|
|
|
123
123
|
},
|
|
124
124
|
deleteFileTitle : {
|
|
125
125
|
en: "Are you sure you want to delete the file \"{{file}}\"? ",
|
|
126
|
-
el: "
|
|
126
|
+
el: "Σίγουρα θέλετε να διαγράψετε το αρχείο \"{{file}}\";",
|
|
127
127
|
tr: "Are you sure you want to delete the file \"{{file}}\"? "
|
|
128
128
|
},
|
|
129
129
|
deleteYesOption: {
|
|
@@ -145,6 +145,11 @@ export const staticResources = {
|
|
|
145
145
|
en: "View file",
|
|
146
146
|
el: "Προβολή αρχείου",
|
|
147
147
|
tr: "View file"
|
|
148
|
+
},
|
|
149
|
+
deleteSameFileWarning: {
|
|
150
|
+
en: "Υou have uploaded the same file more than once in this application. If you delete it, it will be deleted from all places in the application.",
|
|
151
|
+
el: "Έχετε ανεβάσει το αρχείο αυτό και σε άλλα σημεία της αίτησης. Αν το διαγράψετε, θα διαγραφεί από όλα τα σημεία.",
|
|
152
|
+
tr: "Υou have uploaded the same file more than once in this application. If you delete it, it will be deleted from all places in the application."
|
|
148
153
|
}
|
|
149
154
|
},
|
|
150
155
|
//remderer sections
|
|
@@ -102,7 +102,7 @@ export function storePageData(store, siteId, pageUrl, formData) {
|
|
|
102
102
|
export function storePageDataElement(store, siteId, pageUrl, elementName, value) {
|
|
103
103
|
// Ensure session structure is initialized
|
|
104
104
|
initializeSiteData(store, siteId, pageUrl);
|
|
105
|
-
|
|
105
|
+
|
|
106
106
|
// Store the element value
|
|
107
107
|
store.siteData[siteId].inputData[pageUrl].formData[elementName] = value;
|
|
108
108
|
}
|
|
@@ -191,11 +191,11 @@ export function storeSiteSubmissionData(store, siteId, submissionData) {
|
|
|
191
191
|
// let rawData = getSiteInputData(store, siteId);
|
|
192
192
|
// Store the submission data
|
|
193
193
|
store.siteData[siteId].submissionData = submissionData;
|
|
194
|
-
|
|
194
|
+
|
|
195
195
|
// Clear validation errors from the session
|
|
196
196
|
store.siteData[siteId].inputData = {};
|
|
197
197
|
// Clear presaved/temporary save data
|
|
198
|
-
store.siteData[siteId].loadData = {};
|
|
198
|
+
store.siteData[siteId].loadData = {};
|
|
199
199
|
|
|
200
200
|
}
|
|
201
201
|
|
|
@@ -208,7 +208,7 @@ export function storeSiteSubmissionData(store, siteId, submissionData) {
|
|
|
208
208
|
* @param {object} result - API response
|
|
209
209
|
*/
|
|
210
210
|
export function storeSiteEligibilityResult(store, siteId, endpointKey, result) {
|
|
211
|
-
|
|
211
|
+
|
|
212
212
|
initializeSiteData(store, siteId); // Ensure the structure exists
|
|
213
213
|
|
|
214
214
|
if (!store.siteData[siteId].eligibility) store.siteData[siteId].eligibility = {};
|
|
@@ -244,7 +244,7 @@ export function getSiteEligibilityResult(store, siteId, endpointKey, maxAgeMs =
|
|
|
244
244
|
*/
|
|
245
245
|
export function getPageValidationErrors(store, siteId, pageUrl) {
|
|
246
246
|
const validationErrors = store?.siteData?.[siteId]?.inputData?.[pageUrl]?.validationErrors || null;
|
|
247
|
-
|
|
247
|
+
|
|
248
248
|
if (validationErrors) {
|
|
249
249
|
// Clear validation errors from the session
|
|
250
250
|
delete store.siteData[siteId].inputData[pageUrl].validationErrors;
|
|
@@ -275,7 +275,7 @@ export function getPageData(store, siteId, pageUrl) {
|
|
|
275
275
|
*/
|
|
276
276
|
export function getSiteSubmissionErrors(store, siteId) {
|
|
277
277
|
const validationErrors = store?.siteData?.[siteId]?.submissionErrors || null;
|
|
278
|
-
|
|
278
|
+
|
|
279
279
|
if (validationErrors) {
|
|
280
280
|
// Clear validation errors from the session
|
|
281
281
|
delete store.siteData[siteId].submissionErrors;
|
|
@@ -294,7 +294,7 @@ export function getSiteSubmissionErrors(store, siteId) {
|
|
|
294
294
|
*/
|
|
295
295
|
export function getSiteData(store, siteId) {
|
|
296
296
|
const inputData = store?.siteData?.[siteId] || {};
|
|
297
|
-
|
|
297
|
+
|
|
298
298
|
if (inputData) {
|
|
299
299
|
return inputData;
|
|
300
300
|
}
|
|
@@ -311,7 +311,7 @@ export function getSiteData(store, siteId) {
|
|
|
311
311
|
*/
|
|
312
312
|
export function getSiteInputData(store, siteId) {
|
|
313
313
|
const inputData = store?.siteData?.[siteId]?.inputData || {};
|
|
314
|
-
|
|
314
|
+
|
|
315
315
|
if (inputData) {
|
|
316
316
|
return inputData;
|
|
317
317
|
}
|
|
@@ -328,7 +328,7 @@ export function getSiteInputData(store, siteId) {
|
|
|
328
328
|
*/
|
|
329
329
|
export function getSiteLoadData(store, siteId) {
|
|
330
330
|
const loadData = store?.siteData?.[siteId]?.loadData || {};
|
|
331
|
-
|
|
331
|
+
|
|
332
332
|
if (loadData) {
|
|
333
333
|
return loadData;
|
|
334
334
|
}
|
|
@@ -344,9 +344,9 @@ export function getSiteLoadData(store, siteId) {
|
|
|
344
344
|
* @returns {string|null} The reference number or null if not available
|
|
345
345
|
*/
|
|
346
346
|
export function getSiteLoadDataReferenceNumber(store, siteId) {
|
|
347
|
-
|
|
347
|
+
const ref = store?.siteData?.[siteId]?.loadData?.referenceValue;
|
|
348
348
|
|
|
349
|
-
|
|
349
|
+
return typeof ref === 'string' && ref.trim() !== '' ? ref : null;
|
|
350
350
|
}
|
|
351
351
|
|
|
352
352
|
|
|
@@ -359,9 +359,9 @@ export function getSiteLoadDataReferenceNumber(store, siteId) {
|
|
|
359
359
|
*/
|
|
360
360
|
export function getSiteSubmissionData(store, siteId) {
|
|
361
361
|
initializeSiteData(store, siteId); // Ensure the structure exists
|
|
362
|
-
|
|
362
|
+
|
|
363
363
|
const submission = store?.siteData?.[siteId]?.submissionData || {};
|
|
364
|
-
|
|
364
|
+
|
|
365
365
|
if (submission) {
|
|
366
366
|
return submission;
|
|
367
367
|
}
|
|
@@ -388,7 +388,7 @@ export function getFormDataValue(store, siteId, pageUrl, elementName) {
|
|
|
388
388
|
* @param {object} store The session store
|
|
389
389
|
* @returns The user object from the store or null if it doesn't exist.
|
|
390
390
|
*/
|
|
391
|
-
export function getUser(store){
|
|
391
|
+
export function getUser(store) {
|
|
392
392
|
return store.user || null;
|
|
393
393
|
}
|
|
394
394
|
|
|
@@ -396,3 +396,181 @@ export function clearSiteData(store, siteId) {
|
|
|
396
396
|
delete store?.siteData[siteId];
|
|
397
397
|
}
|
|
398
398
|
|
|
399
|
+
/**
|
|
400
|
+
* Check if a file reference is used in more than one place (field) across the site's inputData.
|
|
401
|
+
*
|
|
402
|
+
* A "file reference" is an object like:
|
|
403
|
+
* { sha256: "abc...", fileId: "xyz..." }
|
|
404
|
+
*
|
|
405
|
+
* Matching rules:
|
|
406
|
+
* - If both fileId and sha256 are provided, both must match.
|
|
407
|
+
* - If only one is provided, we match by that single property.
|
|
408
|
+
*
|
|
409
|
+
* Notes:
|
|
410
|
+
* - Does NOT mutate the session.
|
|
411
|
+
* - Safely handles missing site/pages.
|
|
412
|
+
* - If a form field is an array (e.g., multiple file inputs), each item is checked.
|
|
413
|
+
*
|
|
414
|
+
* @param {object} store The session object (e.g., req.session)
|
|
415
|
+
* @param {string} siteId The site identifier
|
|
416
|
+
* @param {object} params { fileId?: string, sha256?: string }
|
|
417
|
+
* @returns {boolean} true if the file is found in more than one place, else false
|
|
418
|
+
*/
|
|
419
|
+
export function isFileUsedInSiteInputDataAgain(store, siteId, { fileId, sha256 } = {}) {
|
|
420
|
+
// If neither identifier is provided, we cannot match anything
|
|
421
|
+
if (!fileId && !sha256) return false;
|
|
422
|
+
|
|
423
|
+
// Ensure session structure is initialized
|
|
424
|
+
initializeSiteData(store, siteId);
|
|
425
|
+
|
|
426
|
+
// Site input data: session.siteData[siteId].inputData
|
|
427
|
+
const site = store?.siteData?.[siteId]?.inputData;
|
|
428
|
+
if (!site || typeof site !== 'object') return false;
|
|
429
|
+
|
|
430
|
+
let hits = 0; // how many fields across the site reference this file
|
|
431
|
+
|
|
432
|
+
// Loop all pages under the site
|
|
433
|
+
for (const pageKey of Object.keys(site)) {
|
|
434
|
+
const formData = site[pageKey]?.formData;
|
|
435
|
+
if (!formData || typeof formData !== 'object') continue;
|
|
436
|
+
|
|
437
|
+
// Loop all fields on the page
|
|
438
|
+
for (const [_, value] of Object.entries(formData)) {
|
|
439
|
+
if (value == null) continue;
|
|
440
|
+
|
|
441
|
+
// Normalize to an array to also support multi-value fields (e.g., multiple file inputs)
|
|
442
|
+
const candidates = Array.isArray(value) ? value : [value];
|
|
443
|
+
|
|
444
|
+
for (const candidate of candidates) {
|
|
445
|
+
// We only consider objects that look like file references
|
|
446
|
+
if (
|
|
447
|
+
candidate &&
|
|
448
|
+
typeof candidate === 'object' &&
|
|
449
|
+
'fileId' in candidate &&
|
|
450
|
+
'sha256' in candidate
|
|
451
|
+
) {
|
|
452
|
+
const idMatches = fileId ? candidate.fileId === fileId : true;
|
|
453
|
+
const shaMatches = sha256 ? candidate.sha256 === sha256 : true;
|
|
454
|
+
|
|
455
|
+
if (idMatches && shaMatches) {
|
|
456
|
+
hits += 1;
|
|
457
|
+
// As soon as we see it in more than one place, we can answer true
|
|
458
|
+
if (hits > 1) return true;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// If we get here, it was used 0 or 1 times
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Remove (replace with "") file values across ALL pages of a site,
|
|
472
|
+
* matching a specific fileId and/or sha256.
|
|
473
|
+
*
|
|
474
|
+
* Matching rules:
|
|
475
|
+
* - If BOTH fileId and sha256 are provided, a file object must match BOTH.
|
|
476
|
+
* - If ONLY fileId is provided, match on fileId.
|
|
477
|
+
* - If ONLY sha256 is provided, match on sha256.
|
|
478
|
+
*
|
|
479
|
+
* Scope:
|
|
480
|
+
* - Operates on every page under store.siteData[siteId].inputData.
|
|
481
|
+
* - Shallow traversal of formData:
|
|
482
|
+
* • Direct fields: formData[elementId] = { fileId, sha256, ... }
|
|
483
|
+
* • Arrays: [ {fileId...}, {sha256...}, "other" ] → ["", "", "other"] (for matches)
|
|
484
|
+
*
|
|
485
|
+
* Side effects:
|
|
486
|
+
* - Mutates store.siteData[siteId].inputData[*].formData in place.
|
|
487
|
+
* - Intentionally returns nothing.
|
|
488
|
+
*
|
|
489
|
+
* @param {object} store - The data-layer store.
|
|
490
|
+
* @param {string} siteId - The site key under store.siteData to modify.
|
|
491
|
+
* @param {{ fileId?: string|null, sha256?: string|null }} match - Identifiers to match.
|
|
492
|
+
* Provide at least one of fileId/sha256. If both are given, both must match.
|
|
493
|
+
*/
|
|
494
|
+
export function removeAllFilesFromSite(
|
|
495
|
+
store,
|
|
496
|
+
siteId,
|
|
497
|
+
{ fileId = null, sha256 = null } = {}
|
|
498
|
+
) {
|
|
499
|
+
// Ensure session structure is initialized
|
|
500
|
+
initializeSiteData(store, siteId);
|
|
501
|
+
// --- Guard rails ---------------------------------------------------------
|
|
502
|
+
|
|
503
|
+
// Nothing to remove if neither identifier is provided.
|
|
504
|
+
if (!fileId && !sha256) return;
|
|
505
|
+
|
|
506
|
+
// Per your structure: dig under .inputData for the site's pages.
|
|
507
|
+
const site = store?.siteData?.[siteId]?.inputData;
|
|
508
|
+
if (!site || typeof site !== "object") return;
|
|
509
|
+
|
|
510
|
+
// --- Helpers -------------------------------------------------------------
|
|
511
|
+
|
|
512
|
+
// Is this value a "file-like" object (has fileId and/or sha256)?
|
|
513
|
+
const isFileLike = (v) =>
|
|
514
|
+
v &&
|
|
515
|
+
typeof v === "object" &&
|
|
516
|
+
(Object.prototype.hasOwnProperty.call(v, "fileId") ||
|
|
517
|
+
Object.prototype.hasOwnProperty.call(v, "sha256"));
|
|
518
|
+
|
|
519
|
+
// Does a file-like object match the provided criteria?
|
|
520
|
+
const isMatch = (obj) => {
|
|
521
|
+
if (!isFileLike(obj)) return false;
|
|
522
|
+
|
|
523
|
+
// Strict when both are given
|
|
524
|
+
if (fileId && sha256) {
|
|
525
|
+
return obj.fileId === fileId && obj.sha256 === sha256;
|
|
526
|
+
}
|
|
527
|
+
// Otherwise match whichever was provided
|
|
528
|
+
if (fileId) return obj.fileId === fileId;
|
|
529
|
+
if (sha256) return obj.sha256 === sha256;
|
|
530
|
+
|
|
531
|
+
return false;
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
// --- Main traversal over all pages --------------------------------------
|
|
535
|
+
|
|
536
|
+
for (const page of Object.values(site)) {
|
|
537
|
+
// Each page should be an object with a formData object
|
|
538
|
+
const formData =
|
|
539
|
+
page &&
|
|
540
|
+
typeof page === "object" &&
|
|
541
|
+
page.formData &&
|
|
542
|
+
typeof page.formData === "object"
|
|
543
|
+
? page.formData
|
|
544
|
+
: null;
|
|
545
|
+
|
|
546
|
+
if (!formData) continue; // skip content-only pages, etc.
|
|
547
|
+
|
|
548
|
+
// For each field on this page…
|
|
549
|
+
for (const key of Object.keys(formData)) {
|
|
550
|
+
const val = formData[key];
|
|
551
|
+
|
|
552
|
+
// Case A: a single file object → replace with "" if it matches.
|
|
553
|
+
if (isMatch(val)) {
|
|
554
|
+
formData[key] = "";
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Case B: an array → replace ONLY the matching items with "".
|
|
559
|
+
if (Array.isArray(val)) {
|
|
560
|
+
let changed = false;
|
|
561
|
+
const mapped = val.map((item) => {
|
|
562
|
+
if (isMatch(item)) {
|
|
563
|
+
changed = true;
|
|
564
|
+
return "";
|
|
565
|
+
}
|
|
566
|
+
return item;
|
|
567
|
+
});
|
|
568
|
+
if (changed) formData[key] = mapped;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Note: If you later store file-like objects deeper in nested objects,
|
|
572
|
+
// add a recursive visitor here (with cycle protection / max depth).
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|