@gov-cy/govcy-express-services 1.0.0-alpha.2 → 1.0.0-alpha.20
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 +1051 -83
- package/package.json +10 -3
- package/src/auth/cyLoginAuth.mjs +2 -1
- package/src/index.mjs +20 -1
- package/src/middleware/cyLoginAuth.mjs +11 -1
- package/src/middleware/govcyCsrf.mjs +15 -1
- package/src/middleware/govcyFileDeleteHandler.mjs +320 -0
- package/src/middleware/govcyFileUpload.mjs +36 -0
- package/src/middleware/govcyFileViewHandler.mjs +161 -0
- package/src/middleware/govcyFormsPostHandler.mjs +1 -1
- package/src/middleware/govcyHttpErrorHandler.mjs +4 -3
- package/src/middleware/govcyPDFRender.mjs +3 -1
- package/src/middleware/govcyPageHandler.mjs +3 -3
- 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 +299 -0
- package/src/public/js/govcyForms.js +19 -8
- package/src/resources/govcyResources.mjs +85 -4
- package/src/utils/govcyApiDetection.mjs +17 -0
- package/src/utils/govcyApiRequest.mjs +30 -5
- package/src/utils/govcyApiResponse.mjs +31 -0
- package/src/utils/govcyConstants.mjs +5 -1
- package/src/utils/govcyDataLayer.mjs +211 -11
- package/src/utils/govcyExpressions.mjs +1 -1
- package/src/utils/govcyFormHandling.mjs +81 -5
- package/src/utils/govcyHandleFiles.mjs +307 -0
- package/src/utils/govcyLoadSubmissionDataAPIs.mjs +10 -3
- package/src/utils/govcySubmitData.mjs +186 -106
- package/src/utils/govcyTempSave.mjs +2 -1
- package/src/utils/govcyValidator.mjs +7 -0
|
@@ -6,6 +6,11 @@ export const staticResources = {
|
|
|
6
6
|
el: "Υποβολή",
|
|
7
7
|
tr: "Gönder"
|
|
8
8
|
},
|
|
9
|
+
continue: {
|
|
10
|
+
en: "Continue",
|
|
11
|
+
el: "Συνέχεια",
|
|
12
|
+
tr: "Continue"
|
|
13
|
+
},
|
|
9
14
|
cancel: {
|
|
10
15
|
en: "Cancel",
|
|
11
16
|
el: "Ακύρωση",
|
|
@@ -54,7 +59,7 @@ export const staticResources = {
|
|
|
54
59
|
errorPage403NaturalOnlyPolicyBody: {
|
|
55
60
|
el: "<p>Η πρόσβαση επιτρέπεται μόνο σε φυσικά πρόσωπα με επιβεβαιωμένο προφίλ. <a href=\"/logout\">Αποσυνδεθείτε</a> και δοκιμάστε ξανά αργότερα.</p>",
|
|
56
61
|
en: "<p>Access is only allowed to individuals with a verified profile.<a href=\"/logout\">Sign out</a> and try again later.</p>",
|
|
57
|
-
tr: "<p>Access is only allowed to individuals with a
|
|
62
|
+
tr: "<p>Access is only allowed to individuals with a verified profile.<a href=\"/logout\">Giriş yapmadan</a> sonra tekrar deneyiniz.</p>"
|
|
58
63
|
},
|
|
59
64
|
errorPage500Title: {
|
|
60
65
|
el: "Λυπούμαστε, υπάρχει πρόβλημα με την υπηρεσία",
|
|
@@ -100,6 +105,51 @@ export const staticResources = {
|
|
|
100
105
|
en: "We have received your request. ",
|
|
101
106
|
el: "Έχουμε λάβει την αίτησή σας. ",
|
|
102
107
|
tr: "We have received your request. "
|
|
108
|
+
},
|
|
109
|
+
fileUploaded : {
|
|
110
|
+
en: "File uploaded",
|
|
111
|
+
el: "Το αρχείο ανεβάστηκε",
|
|
112
|
+
tr: "File uploaded"
|
|
113
|
+
},
|
|
114
|
+
fileNotUploaded : {
|
|
115
|
+
en: "File has not been uploaded. ",
|
|
116
|
+
el: "Το αρχείο δεν ανεβάστηκε. ",
|
|
117
|
+
tr: "File has not been uploaded. "
|
|
118
|
+
},
|
|
119
|
+
fileYouHaveUploaded : {
|
|
120
|
+
en: "You have uploaded the file for \"{{file}}\"",
|
|
121
|
+
el: "Έχετε ανεβάσει το αρχείο \"{{file}}\"",
|
|
122
|
+
tr: "You have uploaded the file for \"{{file}}\""
|
|
123
|
+
},
|
|
124
|
+
deleteFileTitle : {
|
|
125
|
+
en: "Are you sure you want to delete the file \"{{file}}\"? ",
|
|
126
|
+
el: "Σίγουρα θέλετε να διαγράψετε το αρχείο \"{{file}}\";",
|
|
127
|
+
tr: "Are you sure you want to delete the file \"{{file}}\"? "
|
|
128
|
+
},
|
|
129
|
+
deleteYesOption: {
|
|
130
|
+
el:"Ναι, θέλω να διαγράψω το αρχείο",
|
|
131
|
+
en:"Yes, I want to delete this file",
|
|
132
|
+
tr:"Yes, I want to delete this file"
|
|
133
|
+
},
|
|
134
|
+
deleteNoOption: {
|
|
135
|
+
el:"Όχι, δεν θέλω να διαγράψω το αρχείο",
|
|
136
|
+
en:"No, I don't want to delete this file",
|
|
137
|
+
tr:"No, I don't want to delete this file"
|
|
138
|
+
},
|
|
139
|
+
deleteFileValidationError: {
|
|
140
|
+
en: "Select if you want to delete the file",
|
|
141
|
+
el: "Επιλέξτε αν θέλετε να διαγράψετε το αρχείο",
|
|
142
|
+
tr: "Select if you want to delete the file"
|
|
143
|
+
},
|
|
144
|
+
viewFile: {
|
|
145
|
+
en: "View file",
|
|
146
|
+
el: "Προβολή αρχείου",
|
|
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."
|
|
103
153
|
}
|
|
104
154
|
},
|
|
105
155
|
//remderer sections
|
|
@@ -113,9 +163,19 @@ export const staticResources = {
|
|
|
113
163
|
element: "htmlElement",
|
|
114
164
|
params: {
|
|
115
165
|
text: {
|
|
116
|
-
en: `<script src="/js/govcyForms.js"></script>`,
|
|
117
|
-
el: `<script src="/js/govcyForms.js"></script>`,
|
|
118
|
-
tr: `<script src="/js/govcyForms.js"></script>`
|
|
166
|
+
en: `<script src="https://cdn.jsdelivr.net/gh/gov-cy/govcy-frontend-renderer@v1.22.0/dist/govcyCompiledTemplates.browser.js"></script><script src="https://cdn.jsdelivr.net/gh/gov-cy/govcy-frontend-renderer@v1.22.0/dist/govcyFrontendRenderer.browser.js"></script><script type="module" src="/js/govcyForms.js"></script><script type="module" src="/js/govcyFiles.js"></script>`,
|
|
167
|
+
el: `<script src="https://cdn.jsdelivr.net/gh/gov-cy/govcy-frontend-renderer@v1.22.0/dist/govcyCompiledTemplates.browser.js"></script><script src="https://cdn.jsdelivr.net/gh/gov-cy/govcy-frontend-renderer@v1.22.0/dist/govcyFrontendRenderer.browser.js"></script><script type="module" src="/js/govcyForms.js"></script><script type="module" src="/js/govcyFiles.js"></script>`,
|
|
168
|
+
tr: `<script src="https://cdn.jsdelivr.net/gh/gov-cy/govcy-frontend-renderer@v1.22.0/dist/govcyCompiledTemplates.browser.js"></script><script src="https://cdn.jsdelivr.net/gh/gov-cy/govcy-frontend-renderer@v1.22.0/dist/govcyFrontendRenderer.browser.js"></script><script type="module" src="/js/govcyForms.js"></script><script type="module" src="/js/govcyFiles.js"></script>`
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
govcyLoadingOverlay: {
|
|
173
|
+
element: "htmlElement",
|
|
174
|
+
params: {
|
|
175
|
+
text: {
|
|
176
|
+
en: `<style>.govcy-loadingOverlay{position:fixed;top:0;right:0;bottom:0;left:0;display:none;justify-content:center;align-items:center;background:rgba(255,255,255,.7);-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);z-index:1050}.govcy-loadingOverlay[aria-hidden="false"]{display:flex}</style><div id="govcy--loadingOverlay" class="govcy-loadingOverlay" aria-hidden="true" role="dialog" aria-modal="true" tabindex="-1"><div class="govcy-loadingOverlay__content" role="status" aria-live="polite"><div class="spinner-border govcy-text-primary" role="status"><span class="govcy-visually-hidden">Loading...</span></div></div></div>`,
|
|
177
|
+
el: `<style>.govcy-loadingOverlay{position:fixed;top:0;right:0;bottom:0;left:0;display:none;justify-content:center;align-items:center;background:rgba(255,255,255,.7);-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);z-index:1050}.govcy-loadingOverlay[aria-hidden="false"]{display:flex}</style><div id="govcy--loadingOverlay" class="govcy-loadingOverlay" aria-hidden="true" role="dialog" aria-modal="true" tabindex="-1"><div class="govcy-loadingOverlay__content" role="status" aria-live="polite"><div class="spinner-border govcy-text-primary" role="status"><span class="govcy-visually-hidden">Φόρτωση...</span></div></div></div>`,
|
|
178
|
+
tr: `<style>.govcy-loadingOverlay{position:fixed;top:0;right:0;bottom:0;left:0;display:none;justify-content:center;align-items:center;background:rgba(255,255,255,.7);-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);z-index:1050}.govcy-loadingOverlay[aria-hidden="false"]{display:flex}</style><div id="govcy--loadingOverlay" class="govcy-loadingOverlay" aria-hidden="true" role="dialog" aria-modal="true" tabindex="-1"><div class="govcy-loadingOverlay__content" role="status" aria-live="polite"><div class="spinner-border govcy-text-primary" role="status"><span class="govcy-visually-hidden">Loading...</span></div></div></div>`
|
|
119
179
|
}
|
|
120
180
|
}
|
|
121
181
|
},
|
|
@@ -192,6 +252,27 @@ export function csrfTokenInput(csrfToken) {
|
|
|
192
252
|
};
|
|
193
253
|
}
|
|
194
254
|
|
|
255
|
+
/**
|
|
256
|
+
* Get the site and page input elements
|
|
257
|
+
* @param {string} siteId The site id
|
|
258
|
+
* @param {string} pageUrl The page url
|
|
259
|
+
* @param {string} lang The page language
|
|
260
|
+
* @returns {object} htmlElement with the site and page inputs
|
|
261
|
+
*/
|
|
262
|
+
export function siteAndPageInput(siteId, pageUrl, lang = "el") {
|
|
263
|
+
const siteAndPageInputs = `<input type="hidden" name="_siteId" value="${siteId}"><input type="hidden" name="_pageUrl" value="${pageUrl}"><input type="hidden" name="_lang" value="${lang}">`;
|
|
264
|
+
return {
|
|
265
|
+
element: "htmlElement",
|
|
266
|
+
params: {
|
|
267
|
+
text: {
|
|
268
|
+
en: siteAndPageInputs,
|
|
269
|
+
el: siteAndPageInputs,
|
|
270
|
+
tr: siteAndPageInputs
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
195
276
|
/**
|
|
196
277
|
* Error page template
|
|
197
278
|
* @param {object} title the title text element
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Determines if a request is targeting an API endpoint.
|
|
3
|
+
* Currently matches:
|
|
4
|
+
* - Accept header with application/json
|
|
5
|
+
* - URLs ending with /upload or /download under a site/page structure
|
|
6
|
+
*
|
|
7
|
+
* @param {object} req - Express request object
|
|
8
|
+
* @returns {boolean}
|
|
9
|
+
*/
|
|
10
|
+
export function isApiRequest(req) {
|
|
11
|
+
const acceptJson = (req.headers?.accept || "").toLowerCase().includes("application/json");
|
|
12
|
+
|
|
13
|
+
const apiUrlPattern = /^\/apis\/[^/]+\/[^/]+\/(upload|download)$/;
|
|
14
|
+
const isStructuredApiUrl = apiUrlPattern.test(req.originalUrl || req.url);
|
|
15
|
+
|
|
16
|
+
return acceptJson || isStructuredApiUrl;
|
|
17
|
+
}
|
|
@@ -5,12 +5,13 @@ import { logger } from "./govcyLogger.mjs";
|
|
|
5
5
|
* Utility to handle API communication with retry logic
|
|
6
6
|
* @param {string} method - HTTP method (e.g., 'post', 'get', etc.)
|
|
7
7
|
* @param {string} url - API endpoint URL
|
|
8
|
-
* @param {object} inputData - Payload for the request (optional)
|
|
8
|
+
* @param {object|FormData} inputData - Payload for the request (optional)
|
|
9
9
|
* @param {boolean} useAccessTokenAuth - Whether to use Authorization header with Bearer token
|
|
10
10
|
* @param {object} user - User object containing access_token (optional)
|
|
11
11
|
* @param {object} headers - Custom headers (optional)
|
|
12
12
|
* @param {number} retries - Number of retry attempts (default: 3)
|
|
13
13
|
* @param {boolean} allowSelfSignedCerts - Whether to allow self-signed certificates (default: false)
|
|
14
|
+
* @param {array} allowedHTTPStatusCodes - Array of allowed HTTP status codes (default: [200])
|
|
14
15
|
* @returns {Promise<object>} - API response
|
|
15
16
|
*/
|
|
16
17
|
export async function govcyApiRequest(
|
|
@@ -21,7 +22,8 @@ export async function govcyApiRequest(
|
|
|
21
22
|
user = null,
|
|
22
23
|
headers = {},
|
|
23
24
|
retries = 3,
|
|
24
|
-
allowSelfSignedCerts = false
|
|
25
|
+
allowSelfSignedCerts = false,
|
|
26
|
+
allowedHTTPStatusCodes = [200]
|
|
25
27
|
) {
|
|
26
28
|
let attempt = 0;
|
|
27
29
|
|
|
@@ -37,6 +39,14 @@ export async function govcyApiRequest(
|
|
|
37
39
|
requestHeaders['Authorization'] = `Bearer ${user.access_token}`;
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
// If inputData is FormData, for attachments
|
|
43
|
+
if (inputData instanceof (await import('form-data')).default) {
|
|
44
|
+
requestHeaders = {
|
|
45
|
+
...requestHeaders,
|
|
46
|
+
...inputData.getHeaders(), // includes boundary in content-type
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
40
50
|
while (attempt < retries) {
|
|
41
51
|
try {
|
|
42
52
|
logger.debug(`📤 Sending API request (Attempt ${attempt + 1})`, { method, url, inputData, requestHeaders });
|
|
@@ -45,11 +55,22 @@ export async function govcyApiRequest(
|
|
|
45
55
|
const axiosConfig = {
|
|
46
56
|
method,
|
|
47
57
|
url,
|
|
48
|
-
|
|
58
|
+
...(inputData instanceof (await import('form-data')).default // If inputData is FormData, for attachments
|
|
59
|
+
? { data: inputData }
|
|
60
|
+
: { [method?.toLowerCase() === 'get' ? 'params' : 'data']: inputData }),
|
|
49
61
|
headers: requestHeaders,
|
|
50
62
|
timeout: 10000, // 10 seconds timeout
|
|
63
|
+
// ✅ Treat only these statuses as "resolved" (no throw)
|
|
64
|
+
validateStatus: (status) => allowedHTTPStatusCodes.includes(status),
|
|
51
65
|
};
|
|
52
66
|
|
|
67
|
+
// If inputData is FormData, for attachments
|
|
68
|
+
if (inputData instanceof (await import('form-data')).default) {
|
|
69
|
+
axiosConfig.maxContentLength = Infinity;
|
|
70
|
+
axiosConfig.maxBodyLength = Infinity;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
53
74
|
// Add httpsAgent if NOT production to allow self-signed certificates
|
|
54
75
|
// Use per-call config for self-signed certs
|
|
55
76
|
if (allowSelfSignedCerts) {
|
|
@@ -60,9 +81,13 @@ export async function govcyApiRequest(
|
|
|
60
81
|
|
|
61
82
|
logger.debug(`📥 Received API response`, { status: response.status, data: response.data });
|
|
62
83
|
|
|
63
|
-
|
|
84
|
+
// Validate HTTP status
|
|
85
|
+
if (!allowedHTTPStatusCodes.includes(response.status)) {
|
|
64
86
|
throw new Error(`Unexpected HTTP status: ${response.status}`);
|
|
65
87
|
}
|
|
88
|
+
// if (response.status !== 200) {
|
|
89
|
+
// throw new Error(`Unexpected HTTP status: ${response.status}`);
|
|
90
|
+
// }
|
|
66
91
|
|
|
67
92
|
// const { Succeeded, ErrorCode, ErrorMessage } = response.data;
|
|
68
93
|
// Normalize to PascalCase regardless of input case
|
|
@@ -77,7 +102,7 @@ export async function govcyApiRequest(
|
|
|
77
102
|
ErrorMessage,
|
|
78
103
|
Data,
|
|
79
104
|
InformationMessage
|
|
80
|
-
} = response.data;
|
|
105
|
+
} = response.data ?? {};
|
|
81
106
|
|
|
82
107
|
const normalized = {
|
|
83
108
|
Succeeded: Succeeded !== undefined ? Succeeded : succeeded,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The successResponse function creates a standardized success response object.
|
|
3
|
+
*
|
|
4
|
+
* @param {*} data - The data to be included in the response.
|
|
5
|
+
* @returns {Object} - The success response object.
|
|
6
|
+
*/
|
|
7
|
+
export function successResponse(data = null) {
|
|
8
|
+
return {
|
|
9
|
+
Succeeded: true,
|
|
10
|
+
ErrorCode: 0,
|
|
11
|
+
ErrorMessage: '',
|
|
12
|
+
Data: data
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The errorResponse function creates a standardized error response object.
|
|
18
|
+
*
|
|
19
|
+
* @param {int} code - The error code to be included in the response.
|
|
20
|
+
* @param {string} message - The error message to be included in the response.
|
|
21
|
+
* @param {Object} data - Additional data to be included in the response.
|
|
22
|
+
* @returns {Object} - The error response object.
|
|
23
|
+
*/
|
|
24
|
+
export function errorResponse(code = 1, message = 'Unknown error', data = null) {
|
|
25
|
+
return {
|
|
26
|
+
Succeeded: false,
|
|
27
|
+
ErrorCode: code,
|
|
28
|
+
ErrorMessage: message,
|
|
29
|
+
Data: data
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared constants for allowed form elements.
|
|
3
3
|
*/
|
|
4
|
-
export const ALLOWED_FORM_ELEMENTS = ["textInput", "textArea", "select", "radios", "checkboxes", "datePicker", "dateInput"];
|
|
4
|
+
export const ALLOWED_FORM_ELEMENTS = ["textInput", "textArea", "select", "radios", "checkboxes", "datePicker", "dateInput","fileInput","fileView"];
|
|
5
|
+
export const ALLOWED_FILE_MIME_TYPES = ['application/pdf', 'image/jpeg', 'image/png'];
|
|
6
|
+
export const ALLOWED_FILE_EXTENSIONS = ['pdf', 'jpg', 'jpeg', 'png'];
|
|
7
|
+
export const ALLOWED_FILE_SIZE_MB = 4; // Maximum file size in MB
|
|
8
|
+
export const ALLOWED_MULTER_FILE_SIZE_MB = 10; // Maximum file size in MB
|
|
@@ -98,6 +98,14 @@ export function storePageData(store, siteId, pageUrl, formData) {
|
|
|
98
98
|
|
|
99
99
|
store.siteData[siteId].inputData[pageUrl]["formData"] = formData;
|
|
100
100
|
}
|
|
101
|
+
|
|
102
|
+
export function storePageDataElement(store, siteId, pageUrl, elementName, value) {
|
|
103
|
+
// Ensure session structure is initialized
|
|
104
|
+
initializeSiteData(store, siteId, pageUrl);
|
|
105
|
+
|
|
106
|
+
// Store the element value
|
|
107
|
+
store.siteData[siteId].inputData[pageUrl].formData[elementName] = value;
|
|
108
|
+
}
|
|
101
109
|
/**
|
|
102
110
|
* Stores the page's input data in the data layer
|
|
103
111
|
* *
|
|
@@ -183,11 +191,11 @@ export function storeSiteSubmissionData(store, siteId, submissionData) {
|
|
|
183
191
|
// let rawData = getSiteInputData(store, siteId);
|
|
184
192
|
// Store the submission data
|
|
185
193
|
store.siteData[siteId].submissionData = submissionData;
|
|
186
|
-
|
|
194
|
+
|
|
187
195
|
// Clear validation errors from the session
|
|
188
196
|
store.siteData[siteId].inputData = {};
|
|
189
197
|
// Clear presaved/temporary save data
|
|
190
|
-
store.siteData[siteId].loadData = {};
|
|
198
|
+
store.siteData[siteId].loadData = {};
|
|
191
199
|
|
|
192
200
|
}
|
|
193
201
|
|
|
@@ -200,7 +208,7 @@ export function storeSiteSubmissionData(store, siteId, submissionData) {
|
|
|
200
208
|
* @param {object} result - API response
|
|
201
209
|
*/
|
|
202
210
|
export function storeSiteEligibilityResult(store, siteId, endpointKey, result) {
|
|
203
|
-
|
|
211
|
+
|
|
204
212
|
initializeSiteData(store, siteId); // Ensure the structure exists
|
|
205
213
|
|
|
206
214
|
if (!store.siteData[siteId].eligibility) store.siteData[siteId].eligibility = {};
|
|
@@ -236,7 +244,7 @@ export function getSiteEligibilityResult(store, siteId, endpointKey, maxAgeMs =
|
|
|
236
244
|
*/
|
|
237
245
|
export function getPageValidationErrors(store, siteId, pageUrl) {
|
|
238
246
|
const validationErrors = store?.siteData?.[siteId]?.inputData?.[pageUrl]?.validationErrors || null;
|
|
239
|
-
|
|
247
|
+
|
|
240
248
|
if (validationErrors) {
|
|
241
249
|
// Clear validation errors from the session
|
|
242
250
|
delete store.siteData[siteId].inputData[pageUrl].validationErrors;
|
|
@@ -267,7 +275,7 @@ export function getPageData(store, siteId, pageUrl) {
|
|
|
267
275
|
*/
|
|
268
276
|
export function getSiteSubmissionErrors(store, siteId) {
|
|
269
277
|
const validationErrors = store?.siteData?.[siteId]?.submissionErrors || null;
|
|
270
|
-
|
|
278
|
+
|
|
271
279
|
if (validationErrors) {
|
|
272
280
|
// Clear validation errors from the session
|
|
273
281
|
delete store.siteData[siteId].submissionErrors;
|
|
@@ -286,7 +294,7 @@ export function getSiteSubmissionErrors(store, siteId) {
|
|
|
286
294
|
*/
|
|
287
295
|
export function getSiteData(store, siteId) {
|
|
288
296
|
const inputData = store?.siteData?.[siteId] || {};
|
|
289
|
-
|
|
297
|
+
|
|
290
298
|
if (inputData) {
|
|
291
299
|
return inputData;
|
|
292
300
|
}
|
|
@@ -303,7 +311,7 @@ export function getSiteData(store, siteId) {
|
|
|
303
311
|
*/
|
|
304
312
|
export function getSiteInputData(store, siteId) {
|
|
305
313
|
const inputData = store?.siteData?.[siteId]?.inputData || {};
|
|
306
|
-
|
|
314
|
+
|
|
307
315
|
if (inputData) {
|
|
308
316
|
return inputData;
|
|
309
317
|
}
|
|
@@ -320,7 +328,7 @@ export function getSiteInputData(store, siteId) {
|
|
|
320
328
|
*/
|
|
321
329
|
export function getSiteLoadData(store, siteId) {
|
|
322
330
|
const loadData = store?.siteData?.[siteId]?.loadData || {};
|
|
323
|
-
|
|
331
|
+
|
|
324
332
|
if (loadData) {
|
|
325
333
|
return loadData;
|
|
326
334
|
}
|
|
@@ -328,6 +336,20 @@ export function getSiteLoadData(store, siteId) {
|
|
|
328
336
|
return null;
|
|
329
337
|
}
|
|
330
338
|
|
|
339
|
+
/**
|
|
340
|
+
* Get the site's reference number from load data from the store
|
|
341
|
+
*
|
|
342
|
+
* @param {object} store The session store
|
|
343
|
+
* @param {string} siteId The site ID
|
|
344
|
+
* @returns {string|null} The reference number or null if not available
|
|
345
|
+
*/
|
|
346
|
+
export function getSiteLoadDataReferenceNumber(store, siteId) {
|
|
347
|
+
const ref = store?.siteData?.[siteId]?.loadData?.referenceValue;
|
|
348
|
+
|
|
349
|
+
return typeof ref === 'string' && ref.trim() !== '' ? ref : null;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
|
|
331
353
|
/**
|
|
332
354
|
* Get the site's input data from the store
|
|
333
355
|
*
|
|
@@ -337,9 +359,9 @@ export function getSiteLoadData(store, siteId) {
|
|
|
337
359
|
*/
|
|
338
360
|
export function getSiteSubmissionData(store, siteId) {
|
|
339
361
|
initializeSiteData(store, siteId); // Ensure the structure exists
|
|
340
|
-
|
|
362
|
+
|
|
341
363
|
const submission = store?.siteData?.[siteId]?.submissionData || {};
|
|
342
|
-
|
|
364
|
+
|
|
343
365
|
if (submission) {
|
|
344
366
|
return submission;
|
|
345
367
|
}
|
|
@@ -366,7 +388,7 @@ export function getFormDataValue(store, siteId, pageUrl, elementName) {
|
|
|
366
388
|
* @param {object} store The session store
|
|
367
389
|
* @returns The user object from the store or null if it doesn't exist.
|
|
368
390
|
*/
|
|
369
|
-
export function getUser(store){
|
|
391
|
+
export function getUser(store) {
|
|
370
392
|
return store.user || null;
|
|
371
393
|
}
|
|
372
394
|
|
|
@@ -374,3 +396,181 @@ export function clearSiteData(store, siteId) {
|
|
|
374
396
|
delete store?.siteData[siteId];
|
|
375
397
|
}
|
|
376
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
|
+
|
|
@@ -124,7 +124,7 @@ export function evaluateExpressionWithFlattening(expression, object, prefix = ''
|
|
|
124
124
|
* @returns {{ result: true } | { result: false, redirect: string }}
|
|
125
125
|
* An object indicating whether to render the page or redirect
|
|
126
126
|
*/
|
|
127
|
-
export function evaluatePageConditions(page, store, siteKey, req) {
|
|
127
|
+
export function evaluatePageConditions(page, store, siteKey, req = {}) {
|
|
128
128
|
// Get conditions array from nested page structure
|
|
129
129
|
const conditions = page?.pageData?.conditions;
|
|
130
130
|
|