@gov-cy/govcy-express-services 1.0.0-alpha.8 → 1.0.0
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 +1024 -74
- package/package.json +9 -3
- package/src/auth/cyLoginAuth.mjs +2 -1
- package/src/index.mjs +13 -2
- package/src/middleware/cyLoginAuth.mjs +3 -1
- package/src/middleware/govcyFileDeleteHandler.mjs +320 -0
- package/src/middleware/{govcyUpload.mjs → govcyFileUpload.mjs} +1 -1
- package/src/middleware/govcyFileViewHandler.mjs +161 -0
- package/src/middleware/govcyPDFRender.mjs +3 -1
- package/src/middleware/govcyPageHandler.mjs +3 -6
- package/src/middleware/govcyPageRender.mjs +10 -0
- package/src/middleware/govcyReviewPageHandler.mjs +4 -1
- package/src/middleware/govcyReviewPostHandler.mjs +1 -1
- package/src/middleware/govcySuccessPageHandler.mjs +2 -3
- package/src/public/js/govcyFiles.js +201 -77
- package/src/public/js/govcyForms.js +19 -8
- package/src/resources/govcyResources.mjs +57 -7
- package/src/utils/govcyConstants.mjs +1 -1
- package/src/utils/govcyDataLayer.mjs +192 -14
- package/src/utils/govcyFormHandling.mjs +8 -4
- package/src/utils/govcyHandleFiles.mjs +23 -13
- package/src/utils/govcySubmitData.mjs +162 -109
|
@@ -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
|
+
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { ALLOWED_FORM_ELEMENTS } from "./govcyConstants.mjs";
|
|
8
8
|
import * as dataLayer from "./govcyDataLayer.mjs";
|
|
9
|
+
import * as govcyResources from "../resources/govcyResources.mjs";
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -17,8 +18,10 @@ import * as dataLayer from "./govcyDataLayer.mjs";
|
|
|
17
18
|
* @param {string} siteId The site ID
|
|
18
19
|
* @param {string} pageUrl The page URL
|
|
19
20
|
* @param {string} lang The language
|
|
21
|
+
* @param {Object} fileInputElements The file input elements
|
|
22
|
+
* @param {string} routeParam The route parameter
|
|
20
23
|
*/
|
|
21
|
-
export function populateFormData(formElements, theData, validationErrors, store = {}, siteId = "", pageUrl = "", lang = "el", fileInputElements = null) {
|
|
24
|
+
export function populateFormData(formElements, theData, validationErrors, store = {}, siteId = "", pageUrl = "", lang = "el", fileInputElements = null, routeParam = "") {
|
|
22
25
|
const inputElements = ALLOWED_FORM_ELEMENTS;
|
|
23
26
|
const isRootCall = !fileInputElements;
|
|
24
27
|
if (isRootCall) {
|
|
@@ -75,8 +78,9 @@ export function populateFormData(formElements, theData, validationErrors, store
|
|
|
75
78
|
element.params.sha256 = fileData.sha256;
|
|
76
79
|
element.params.visuallyHiddenText = element.params.label;
|
|
77
80
|
// TODO: Also need to set the `view` and `download` URLs
|
|
78
|
-
element.params.viewHref =
|
|
79
|
-
element.params.
|
|
81
|
+
element.params.viewHref = `/${siteId}/${pageUrl}/view-file/${fieldName}`;
|
|
82
|
+
element.params.viewTarget = "_blank";
|
|
83
|
+
element.params.deleteHref = `/${siteId}/${pageUrl}/delete-file/${fieldName}${(routeParam) ? `?route=${routeParam}` : ''}`;
|
|
80
84
|
} else {
|
|
81
85
|
// TODO: Ask Andreas how to handle empty file inputs
|
|
82
86
|
element.params.value = "";
|
|
@@ -107,7 +111,7 @@ export function populateFormData(formElements, theData, validationErrors, store
|
|
|
107
111
|
if (element.element === "radios" && element.params.items) {
|
|
108
112
|
element.params.items.forEach(item => {
|
|
109
113
|
if (item.conditionalElements) {
|
|
110
|
-
populateFormData(item.conditionalElements, theData, validationErrors,store, siteId , pageUrl, lang, fileInputElements);
|
|
114
|
+
populateFormData(item.conditionalElements, theData, validationErrors,store, siteId , pageUrl, lang, fileInputElements, routeParam);
|
|
111
115
|
|
|
112
116
|
// Check if any conditional element has an error and add to the parent "conditionalHasErrors": true
|
|
113
117
|
if (item.conditionalElements.some(condEl => condEl.params?.error)) {
|
|
@@ -198,11 +198,11 @@ export async function handleFileUpload({ service, store, siteId, pageUrl, elemen
|
|
|
198
198
|
return {
|
|
199
199
|
status: 200,
|
|
200
200
|
data: {
|
|
201
|
-
|
|
201
|
+
sha256: response.Data.sha256,
|
|
202
202
|
filename: response.Data.fileName || '',
|
|
203
203
|
fileId: response.Data.fileId,
|
|
204
204
|
mimeType: response.Data.contentType || '',
|
|
205
|
-
|
|
205
|
+
fileSize: response.Data?.fileSize || ''
|
|
206
206
|
}
|
|
207
207
|
};
|
|
208
208
|
|
|
@@ -234,11 +234,12 @@ function containsFileInput(elements = [], targetName) {
|
|
|
234
234
|
for (const el of elements) {
|
|
235
235
|
// ✅ Direct file input match
|
|
236
236
|
if (el.element === 'fileInput' && el.params?.name === targetName) {
|
|
237
|
-
return
|
|
237
|
+
return el;
|
|
238
238
|
}
|
|
239
239
|
// 🔁 Recurse into nested elements (e.g. groups, conditionals)
|
|
240
240
|
if (Array.isArray(el?.params?.elements)) {
|
|
241
|
-
|
|
241
|
+
const nestedMatch = containsFileInput(el.params.elements, targetName);
|
|
242
|
+
if (nestedMatch) return nestedMatch; // ← propagate the found element
|
|
242
243
|
}
|
|
243
244
|
|
|
244
245
|
// 🎯 Special case: conditional radios/checkboxes
|
|
@@ -248,7 +249,8 @@ function containsFileInput(elements = [], targetName) {
|
|
|
248
249
|
) {
|
|
249
250
|
for (const item of el.params.items) {
|
|
250
251
|
if (Array.isArray(item?.conditionalElements)) {
|
|
251
|
-
|
|
252
|
+
const match = containsFileInput(item.conditionalElements, targetName);
|
|
253
|
+
if (match) return match; // ← propagate the found element
|
|
252
254
|
}
|
|
253
255
|
}
|
|
254
256
|
}
|
|
@@ -264,23 +266,31 @@ function containsFileInput(elements = [], targetName) {
|
|
|
264
266
|
* @param {string} elementName The name of the element to check
|
|
265
267
|
* @return {boolean} True if a fileInput exists, false otherwise
|
|
266
268
|
*/
|
|
267
|
-
function pageContainsFileInput(pageTemplate, elementName) {
|
|
269
|
+
export function pageContainsFileInput(pageTemplate, elementName) {
|
|
268
270
|
const sections = pageTemplate?.sections || [];
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
271
|
+
|
|
272
|
+
for (const section of sections) {
|
|
273
|
+
for (const el of section?.elements || []) {
|
|
274
|
+
if (el.element === 'form') {
|
|
275
|
+
const match = containsFileInput(el.params?.elements, elementName);
|
|
276
|
+
if (match) {
|
|
277
|
+
return match; // ← return the actual element
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return null; // no match found
|
|
275
284
|
}
|
|
276
285
|
|
|
286
|
+
|
|
277
287
|
/**
|
|
278
288
|
* Validates magic bytes against expected mimetype
|
|
279
289
|
* @param {Buffer} buffer
|
|
280
290
|
* @param {string} mimetype
|
|
281
291
|
* @returns {boolean}
|
|
282
292
|
*/
|
|
283
|
-
function isMagicByteValid(buffer, mimetype) {
|
|
293
|
+
export function isMagicByteValid(buffer, mimetype) {
|
|
284
294
|
const signatures = {
|
|
285
295
|
'application/pdf': [0x25, 0x50, 0x44, 0x46], // %PDF
|
|
286
296
|
'image/png': [0x89, 0x50, 0x4E, 0x47], // PNG
|