@vicacha-devs/strapi-plugin-inbox 1.1.0 → 1.1.1
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/dist/admin/{InboxHomePage-B5xUPD7Y.mjs → InboxHomePage-CfoiAI1Z.mjs} +2 -2
- package/dist/admin/{InboxHomePage-BCHbPE2t.js → InboxHomePage-DPuQPIwL.js} +2 -2
- package/dist/admin/{LocalizationRoutingTab-CEXbHNe4.mjs → LocalizationRoutingTab-C_enaoml.mjs} +1 -1
- package/dist/admin/{LocalizationRoutingTab-Dt6U-kXn.js → LocalizationRoutingTab-DLlVOdSz.js} +1 -1
- package/dist/admin/{SettingsPage-qB_efKCl.js → SettingsPage-CWTOJAEO.js} +2 -2
- package/dist/admin/{SettingsPage-Dif7Cq7I.mjs → SettingsPage-Ctvf4SNg.mjs} +2 -2
- package/dist/admin/{index-5e9dAMBs.mjs → index-CvxBUxrS.mjs} +63 -11
- package/dist/admin/{index-DLfoIVZP.js → index-FRs4UYmW.js} +63 -11
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/server/index.js +281 -119
- package/dist/server/index.mjs +281 -119
- package/dist/server/src/bootstrap.d.ts +1 -0
- package/dist/server/src/routes/admin.d.ts +6 -1
- package/dist/server/src/routes/index.d.ts +6 -1
- package/dist/shared/index.d.ts +2 -1
- package/dist/shared/index.js +185 -115
- package/dist/shared/index.mjs +185 -115
- package/package.json +6 -6
package/dist/server/index.mjs
CHANGED
|
@@ -43,15 +43,122 @@ const DEFAULT_INBOX_SETTINGS = {
|
|
|
43
43
|
defaultNotifyEmails: [],
|
|
44
44
|
outboundWebhookUrl: ""
|
|
45
45
|
};
|
|
46
|
+
const SUPPORTED_INTL_CALENDARS = Object.freeze([
|
|
47
|
+
{ id: "gregory", intlCalendar: "gregory", defaultLabel: "Gregorian" },
|
|
48
|
+
{ id: "iso8601", intlCalendar: "iso8601", defaultLabel: "ISO 8601" },
|
|
49
|
+
{
|
|
50
|
+
id: "islamic-umalqura",
|
|
51
|
+
intlCalendar: "islamic-umalqura",
|
|
52
|
+
defaultLabel: "Hijri (Umm al-Qura)"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: "islamic-civil",
|
|
56
|
+
intlCalendar: "islamic-civil",
|
|
57
|
+
defaultLabel: "Hijri (Civil)"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: "islamic-tbla",
|
|
61
|
+
intlCalendar: "islamic-tbla",
|
|
62
|
+
defaultLabel: "Hijri (Tabular)"
|
|
63
|
+
},
|
|
64
|
+
{ id: "hebrew", intlCalendar: "hebrew", defaultLabel: "Hebrew" },
|
|
65
|
+
{
|
|
66
|
+
id: "persian",
|
|
67
|
+
intlCalendar: "persian",
|
|
68
|
+
defaultLabel: "Persian (Solar Hijri)"
|
|
69
|
+
},
|
|
70
|
+
{ id: "buddhist", intlCalendar: "buddhist", defaultLabel: "Buddhist" },
|
|
71
|
+
{ id: "japanese", intlCalendar: "japanese", defaultLabel: "Japanese" },
|
|
72
|
+
{ id: "chinese", intlCalendar: "chinese", defaultLabel: "Chinese Lunar" },
|
|
73
|
+
{ id: "indian", intlCalendar: "indian", defaultLabel: "Indian National" },
|
|
74
|
+
{ id: "coptic", intlCalendar: "coptic", defaultLabel: "Coptic" },
|
|
75
|
+
{ id: "ethiopic", intlCalendar: "ethiopic", defaultLabel: "Ethiopic" }
|
|
76
|
+
]);
|
|
77
|
+
Object.freeze([
|
|
78
|
+
"none",
|
|
79
|
+
...SUPPORTED_INTL_CALENDARS.map((item) => item.id)
|
|
80
|
+
]);
|
|
81
|
+
const BASE_BCP47_REGEX = /^[a-zA-Z]{2,3}(?:-[a-zA-Z0-9]{2,8})*$/;
|
|
82
|
+
function sanitizeCalendarBaseLocale(locale, fallbackLocale = "es") {
|
|
83
|
+
if (typeof locale !== "string" || !locale.trim()) {
|
|
84
|
+
return fallbackLocale;
|
|
85
|
+
}
|
|
86
|
+
const trimmed = locale.trim();
|
|
87
|
+
const uIdx = trimmed.toLowerCase().indexOf("-u-");
|
|
88
|
+
let stripped = uIdx >= 0 ? trimmed.slice(0, uIdx) : trimmed;
|
|
89
|
+
while (stripped.endsWith("-")) {
|
|
90
|
+
stripped = stripped.slice(0, -1);
|
|
91
|
+
}
|
|
92
|
+
if (!stripped || stripped.length > 35 || !BASE_BCP47_REGEX.test(stripped)) {
|
|
93
|
+
return fallbackLocale;
|
|
94
|
+
}
|
|
95
|
+
return stripped;
|
|
96
|
+
}
|
|
97
|
+
function formatSecondaryDate(isoDateString, calendar = "islamic-umalqura", locale = "es") {
|
|
98
|
+
if (!isoDateString || calendar === "none") {
|
|
99
|
+
return { calendar, formatted: "" };
|
|
100
|
+
}
|
|
101
|
+
const date = new Date(isoDateString);
|
|
102
|
+
if (Number.isNaN(date.getTime())) {
|
|
103
|
+
return { calendar, formatted: isoDateString };
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const baseLocale = sanitizeCalendarBaseLocale(locale);
|
|
107
|
+
const formatter = new Intl.DateTimeFormat(`${baseLocale}-u-ca-${calendar}`, {
|
|
108
|
+
year: "numeric",
|
|
109
|
+
month: "short",
|
|
110
|
+
day: "numeric"
|
|
111
|
+
});
|
|
112
|
+
const parts = formatter.formatToParts(date);
|
|
113
|
+
const year = Number(parts.find((p) => p.type === "year")?.value) || void 0;
|
|
114
|
+
const month = Number(parts.find((p) => p.type === "month")?.value) || void 0;
|
|
115
|
+
const day = Number(parts.find((p) => p.type === "day")?.value) || void 0;
|
|
116
|
+
return {
|
|
117
|
+
calendar,
|
|
118
|
+
formatted: formatter.format(date),
|
|
119
|
+
year,
|
|
120
|
+
month,
|
|
121
|
+
day
|
|
122
|
+
};
|
|
123
|
+
} catch {
|
|
124
|
+
return {
|
|
125
|
+
calendar,
|
|
126
|
+
formatted: date.toISOString().slice(0, 10)
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
46
130
|
function isValidEmail(value) {
|
|
47
|
-
if (
|
|
131
|
+
if (typeof value !== "string" || value.length < 3 || value.length > 254) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
if (/[\u0000-\u0020\u007f]/.test(value) || /\s/.test(value))
|
|
48
135
|
return false;
|
|
49
136
|
const at = value.indexOf("@");
|
|
50
|
-
if (at < 1 || at !== value.lastIndexOf("@"))
|
|
137
|
+
if (at < 1 || at > 64 || at !== value.lastIndexOf("@"))
|
|
138
|
+
return false;
|
|
139
|
+
const local = value.slice(0, at);
|
|
140
|
+
if (local.startsWith(".") || local.endsWith(".") || local.includes("..")) {
|
|
51
141
|
return false;
|
|
142
|
+
}
|
|
52
143
|
const domain = value.slice(at + 1);
|
|
53
|
-
|
|
54
|
-
|
|
144
|
+
if (domain.startsWith(".") || domain.endsWith(".") || domain.includes("..")) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
const labels = domain.split(".");
|
|
148
|
+
if (labels.length < 2)
|
|
149
|
+
return false;
|
|
150
|
+
return labels.every((label) => label.length > 0 && label.length <= 63 && !label.startsWith("-") && !label.endsWith("-"));
|
|
151
|
+
}
|
|
152
|
+
const CSV_FORMULA_PREFIX_RE = /^[\s\u0085\u2028\u2029]*[=+\-@|]|^[\t\r\n]/;
|
|
153
|
+
function escapeCsvCell(value) {
|
|
154
|
+
if (value === null || value === void 0)
|
|
155
|
+
return "";
|
|
156
|
+
const raw = typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
157
|
+
const sanitized = raw.length > 0 && CSV_FORMULA_PREFIX_RE.test(raw) ? `'${raw}` : raw;
|
|
158
|
+
if (sanitized.includes('"') || sanitized.includes(",") || sanitized.includes("\n") || sanitized.includes("\r")) {
|
|
159
|
+
return `"${sanitized.replace(/"/g, '""')}"`;
|
|
160
|
+
}
|
|
161
|
+
return sanitized;
|
|
55
162
|
}
|
|
56
163
|
function freezeOptions(items) {
|
|
57
164
|
return Object.freeze(items.map((item) => Object.freeze({ ...item })));
|
|
@@ -83,6 +190,54 @@ freezeOptions([
|
|
|
83
190
|
{ value: "persian", label: "persian (Solar Hijri)" },
|
|
84
191
|
{ value: "none", label: "none (Gregorian Only)" }
|
|
85
192
|
]);
|
|
193
|
+
class SlidingWindowRateLimiter {
|
|
194
|
+
hits = /* @__PURE__ */ new Map();
|
|
195
|
+
maxTrackedKeys;
|
|
196
|
+
defaultWindowMs;
|
|
197
|
+
constructor(options2) {
|
|
198
|
+
this.maxTrackedKeys = options2?.maxTrackedKeys ?? 1e4;
|
|
199
|
+
this.defaultWindowMs = options2?.defaultWindowMs ?? 60 * 60 * 1e3;
|
|
200
|
+
}
|
|
201
|
+
check(ip, bucketKey, limitPerWindow, nowMs = Date.now(), windowMs = this.defaultWindowMs) {
|
|
202
|
+
if (limitPerWindow <= 0) {
|
|
203
|
+
return { allowed: true, remaining: 999, retryAfterSeconds: 0 };
|
|
204
|
+
}
|
|
205
|
+
if (this.hits.size > this.maxTrackedKeys) {
|
|
206
|
+
const oldestKey = this.hits.keys().next().value;
|
|
207
|
+
if (oldestKey !== void 0) {
|
|
208
|
+
this.hits.delete(oldestKey);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const key = `${bucketKey}::${ip || "unknown"}`;
|
|
212
|
+
const cutoff = nowMs - windowMs;
|
|
213
|
+
const previous = (this.hits.get(key) || []).filter((ts) => ts > cutoff);
|
|
214
|
+
if (previous.length >= limitPerWindow) {
|
|
215
|
+
const oldest = previous[0] ?? nowMs;
|
|
216
|
+
const retryAfterSeconds = Math.max(1, Math.ceil((oldest + windowMs - nowMs) / 1e3));
|
|
217
|
+
this.hits.set(key, previous);
|
|
218
|
+
return {
|
|
219
|
+
allowed: false,
|
|
220
|
+
remaining: 0,
|
|
221
|
+
retryAfterSeconds
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
previous.push(nowMs);
|
|
225
|
+
this.hits.set(key, previous);
|
|
226
|
+
return {
|
|
227
|
+
allowed: true,
|
|
228
|
+
remaining: Math.max(0, limitPerWindow - previous.length),
|
|
229
|
+
retryAfterSeconds: 0
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
reset() {
|
|
233
|
+
this.hits.clear();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const FORBIDDEN_OBJECT_KEYS = /* @__PURE__ */ new Set([
|
|
237
|
+
"__proto__",
|
|
238
|
+
"constructor",
|
|
239
|
+
"prototype"
|
|
240
|
+
]);
|
|
86
241
|
const LOCALIZED_REQUIRED_FALLBACK = {
|
|
87
242
|
es: (label) => `${label} es obligatorio`,
|
|
88
243
|
en: (label) => `${label} is required`,
|
|
@@ -180,12 +335,23 @@ function checkNumericBoundRule(rule, limit, rawValue, fieldLabel, locale) {
|
|
|
180
335
|
}
|
|
181
336
|
return null;
|
|
182
337
|
}
|
|
338
|
+
function hasNestedQuantifier(pattern) {
|
|
339
|
+
return /[+*?}]\s*\)\s*[+*?{]/.test(pattern.replaceAll(/\\./g, ""));
|
|
340
|
+
}
|
|
183
341
|
function checkLengthOrNumberRule(rule, rawValue, strVal, fieldLabel, locale) {
|
|
184
|
-
|
|
185
|
-
|
|
342
|
+
const numRuleVal = typeof rule.value === "number" ? rule.value : typeof rule.value === "string" && rule.value.trim() !== "" ? Number(rule.value) : Number.NaN;
|
|
343
|
+
if (!Number.isFinite(numRuleVal)) return null;
|
|
344
|
+
return checkLengthRule(rule, numRuleVal, strVal, fieldLabel, locale) ?? checkNumericBoundRule(rule, numRuleVal, rawValue, fieldLabel, locale);
|
|
186
345
|
}
|
|
187
346
|
function checkPatternOrEmailRule(rule, fieldType, strVal, fieldLabel, locale) {
|
|
188
347
|
if (rule.type === "pattern" && typeof rule.value === "string") {
|
|
348
|
+
if (strVal.length > 4096 || rule.value.length > 512 || hasNestedQuantifier(rule.value)) {
|
|
349
|
+
return resolveRuleMessage(
|
|
350
|
+
rule,
|
|
351
|
+
locale,
|
|
352
|
+
`${fieldLabel} format is invalid`
|
|
353
|
+
);
|
|
354
|
+
}
|
|
189
355
|
try {
|
|
190
356
|
const regex = new RegExp(rule.value);
|
|
191
357
|
if (!regex.test(strVal)) {
|
|
@@ -205,6 +371,13 @@ function checkPatternOrEmailRule(rule, fieldType, strVal, fieldLabel, locale) {
|
|
|
205
371
|
`${fieldLabel} must be a valid email address`
|
|
206
372
|
);
|
|
207
373
|
}
|
|
374
|
+
if (rule.type === "url" && fieldType !== "url" && !isValidHttpUrl(strVal)) {
|
|
375
|
+
return resolveRuleMessage(
|
|
376
|
+
rule,
|
|
377
|
+
locale,
|
|
378
|
+
`${fieldLabel} must be a valid URL`
|
|
379
|
+
);
|
|
380
|
+
}
|
|
208
381
|
return null;
|
|
209
382
|
}
|
|
210
383
|
function evaluateCustomRules(field, rawValue, strVal, fieldLabel, locale, rules) {
|
|
@@ -239,7 +412,7 @@ function buildRequiredError(rules, locale, fieldLabel) {
|
|
|
239
412
|
return resolveRuleMessage(reqRule, locale, fallbackFn(fieldLabel));
|
|
240
413
|
}
|
|
241
414
|
function validateSingleField(field, data, locale) {
|
|
242
|
-
const rawValue = data[field.name]
|
|
415
|
+
const rawValue = Object.hasOwn(data, field.name) ? data[field.name] : field.defaultValue;
|
|
243
416
|
const fieldLabel = resolveFieldLabel(field, locale);
|
|
244
417
|
const rules = field.validation || [];
|
|
245
418
|
if (field.required && isValueEmpty(rawValue)) {
|
|
@@ -272,7 +445,9 @@ function validateFormSubmission(fields, data, locale = "en") {
|
|
|
272
445
|
const errors = {};
|
|
273
446
|
const cleanedData = {};
|
|
274
447
|
for (const field of fields) {
|
|
275
|
-
if (DECORATIVE_FIELD_TYPES.includes(field.type))
|
|
448
|
+
if (DECORATIVE_FIELD_TYPES.includes(field.type) || FORBIDDEN_OBJECT_KEYS.has(field.name)) {
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
276
451
|
const { fieldErrors, cleanedValue } = validateSingleField(
|
|
277
452
|
field,
|
|
278
453
|
data,
|
|
@@ -291,45 +466,10 @@ function validateFormSubmission(fields, data, locale = "en") {
|
|
|
291
466
|
};
|
|
292
467
|
}
|
|
293
468
|
function isHoneypotTriggered(payload, honeypotField = "_website_url") {
|
|
294
|
-
if (!honeypotField) return false;
|
|
469
|
+
if (!honeypotField || !Object.hasOwn(payload, honeypotField)) return false;
|
|
295
470
|
const val = payload[honeypotField];
|
|
296
471
|
return typeof val === "string" && val.trim().length > 0;
|
|
297
472
|
}
|
|
298
|
-
class SlidingWindowRateLimiter {
|
|
299
|
-
hits = /* @__PURE__ */ new Map();
|
|
300
|
-
check(ip, bucketKey, limitPerHour, nowMs = Date.now()) {
|
|
301
|
-
if (limitPerHour <= 0) {
|
|
302
|
-
return { allowed: true, remaining: 999, retryAfterSeconds: 0 };
|
|
303
|
-
}
|
|
304
|
-
const windowMs = 60 * 60 * 1e3;
|
|
305
|
-
const key = `${bucketKey}::${ip || "unknown"}`;
|
|
306
|
-
const cutoff = nowMs - windowMs;
|
|
307
|
-
const previous = (this.hits.get(key) || []).filter((ts) => ts > cutoff);
|
|
308
|
-
if (previous.length >= limitPerHour) {
|
|
309
|
-
const oldest = previous[0] ?? nowMs;
|
|
310
|
-
const retryAfterSeconds = Math.max(
|
|
311
|
-
1,
|
|
312
|
-
Math.ceil((oldest + windowMs - nowMs) / 1e3)
|
|
313
|
-
);
|
|
314
|
-
this.hits.set(key, previous);
|
|
315
|
-
return {
|
|
316
|
-
allowed: false,
|
|
317
|
-
remaining: 0,
|
|
318
|
-
retryAfterSeconds
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
previous.push(nowMs);
|
|
322
|
-
this.hits.set(key, previous);
|
|
323
|
-
return {
|
|
324
|
-
allowed: true,
|
|
325
|
-
remaining: Math.max(0, limitPerHour - previous.length),
|
|
326
|
-
retryAfterSeconds: 0
|
|
327
|
-
};
|
|
328
|
-
}
|
|
329
|
-
reset() {
|
|
330
|
-
this.hits.clear();
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
473
|
const defaultRateLimiter = new SlidingWindowRateLimiter();
|
|
334
474
|
function resolveFetchRunner(fetchImpl) {
|
|
335
475
|
return fetchImpl ?? globalThis.fetch;
|
|
@@ -688,16 +828,6 @@ function localizeFormForLocale(form, locale) {
|
|
|
688
828
|
}
|
|
689
829
|
};
|
|
690
830
|
}
|
|
691
|
-
const FORMULA_PREFIXES = ["=", "+", "-", "@", " ", "\r"];
|
|
692
|
-
function escapeCsvCell(value) {
|
|
693
|
-
if (value === null || value === void 0) return "";
|
|
694
|
-
const raw = typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
695
|
-
const sanitized = raw.length > 0 && FORMULA_PREFIXES.includes(raw[0]) ? `'${raw}` : raw;
|
|
696
|
-
if (sanitized.includes('"') || sanitized.includes(",") || sanitized.includes("\n") || sanitized.includes("\r")) {
|
|
697
|
-
return `"${sanitized.replace(/"/g, '""')}"`;
|
|
698
|
-
}
|
|
699
|
-
return sanitized;
|
|
700
|
-
}
|
|
701
831
|
function collectDataFieldNames(submissions, fields) {
|
|
702
832
|
const dataFieldNames = [];
|
|
703
833
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -761,36 +891,7 @@ function submissionsToCsv(submissions, fields = []) {
|
|
|
761
891
|
return rows.join("\n");
|
|
762
892
|
}
|
|
763
893
|
function convertIsoDateToCalendar(isoDateString, calendar = "islamic-umalqura", locale = "en") {
|
|
764
|
-
|
|
765
|
-
return { calendar, formatted: "" };
|
|
766
|
-
}
|
|
767
|
-
const date = new Date(isoDateString);
|
|
768
|
-
if (Number.isNaN(date.getTime())) {
|
|
769
|
-
return { calendar, formatted: isoDateString };
|
|
770
|
-
}
|
|
771
|
-
try {
|
|
772
|
-
const formatter = new Intl.DateTimeFormat(`${locale}-u-ca-${calendar}`, {
|
|
773
|
-
year: "numeric",
|
|
774
|
-
month: "short",
|
|
775
|
-
day: "numeric"
|
|
776
|
-
});
|
|
777
|
-
const parts = formatter.formatToParts(date);
|
|
778
|
-
const year = Number(parts.find((p) => p.type === "year")?.value) || void 0;
|
|
779
|
-
const month = Number(parts.find((p) => p.type === "month")?.value) || void 0;
|
|
780
|
-
const day = Number(parts.find((p) => p.type === "day")?.value) || void 0;
|
|
781
|
-
return {
|
|
782
|
-
calendar,
|
|
783
|
-
formatted: formatter.format(date),
|
|
784
|
-
year,
|
|
785
|
-
month,
|
|
786
|
-
day
|
|
787
|
-
};
|
|
788
|
-
} catch {
|
|
789
|
-
return {
|
|
790
|
-
calendar,
|
|
791
|
-
formatted: date.toISOString().slice(0, 10)
|
|
792
|
-
};
|
|
793
|
-
}
|
|
894
|
+
return formatSecondaryDate(isoDateString, calendar, locale);
|
|
794
895
|
}
|
|
795
896
|
let mirroringToFormBuilderCms = false;
|
|
796
897
|
function generateReferenceCode(now = /* @__PURE__ */ new Date()) {
|
|
@@ -1263,8 +1364,14 @@ function extractLifecycleFormId(event) {
|
|
|
1263
1364
|
const fromResult = resultForm !== void 0 ? resolveCmsSubmissionFormRef({ form: resultForm }).formId : null;
|
|
1264
1365
|
return fromResult ?? void 0;
|
|
1265
1366
|
}
|
|
1367
|
+
const subscribedStrapiInstances = /* @__PURE__ */ new WeakSet();
|
|
1368
|
+
function clearSubscribedStrapiInstance(strapi) {
|
|
1369
|
+
subscribedStrapiInstances.delete(strapi);
|
|
1370
|
+
}
|
|
1266
1371
|
function subscribeFormBuilderLifecycles(strapi, formService2, submissionService2) {
|
|
1267
1372
|
if (!strapi.db?.lifecycles?.subscribe) return;
|
|
1373
|
+
if (subscribedStrapiInstances.has(strapi)) return;
|
|
1374
|
+
subscribedStrapiInstances.add(strapi);
|
|
1268
1375
|
if (strapi.contentTypes?.[FORM_BUILDER_CMS_FORM_UID]) {
|
|
1269
1376
|
strapi.db.lifecycles.subscribe({
|
|
1270
1377
|
models: [FORM_BUILDER_CMS_FORM_UID],
|
|
@@ -1503,8 +1610,16 @@ function normalizeSubmitPayload(rawBody) {
|
|
|
1503
1610
|
return nestedData ? { ...rawBody, ...nestedData } : rawBody;
|
|
1504
1611
|
}
|
|
1505
1612
|
function resolveClientIp(ctx) {
|
|
1613
|
+
const directIp = ctx.ip || ctx.request.ip;
|
|
1614
|
+
if (directIp && directIp.trim()) return directIp.trim();
|
|
1506
1615
|
const forwarded = ctx.request.headers?.["x-forwarded-for"]?.split(",")[0]?.trim();
|
|
1507
|
-
return forwarded ||
|
|
1616
|
+
return forwarded || "127.0.0.1";
|
|
1617
|
+
}
|
|
1618
|
+
function isAuthenticatedAdminContext(ctx) {
|
|
1619
|
+
const state = ctx.state;
|
|
1620
|
+
return Boolean(
|
|
1621
|
+
state?.user || state?.isAuthenticatedAdmin || state?.route?.info?.type === "admin"
|
|
1622
|
+
);
|
|
1508
1623
|
}
|
|
1509
1624
|
const ERROR_STATUS_MAP = {
|
|
1510
1625
|
NotFoundError: 404,
|
|
@@ -1539,11 +1654,12 @@ const createFormHandlers = (strapi) => ({
|
|
|
1539
1654
|
},
|
|
1540
1655
|
async listForms(ctx) {
|
|
1541
1656
|
const svc = strapi.plugin(PLUGIN_ID).service("formService");
|
|
1657
|
+
const isAdmin = isAuthenticatedAdminContext(ctx);
|
|
1542
1658
|
const data = await svc.listForms({
|
|
1543
1659
|
category: ctx.query?.category,
|
|
1544
|
-
activeOnly: ctx.query?.activeOnly !== "false",
|
|
1660
|
+
activeOnly: isAdmin ? ctx.query?.activeOnly !== "false" : true,
|
|
1545
1661
|
locale: ctx.query?.locale,
|
|
1546
|
-
publicSafe: ctx.query?.admin !== "true"
|
|
1662
|
+
publicSafe: isAdmin ? ctx.query?.admin !== "true" : true
|
|
1547
1663
|
});
|
|
1548
1664
|
ctx.body = { data };
|
|
1549
1665
|
},
|
|
@@ -1585,8 +1701,14 @@ const createFormHandlers = (strapi) => ({
|
|
|
1585
1701
|
locale: rawBody.locale || ctx.query?.locale,
|
|
1586
1702
|
captchaToken: rawBody.captchaToken || payload.captchaToken
|
|
1587
1703
|
});
|
|
1704
|
+
const {
|
|
1705
|
+
ipAddress: _ipAddress,
|
|
1706
|
+
userAgent: _userAgent,
|
|
1707
|
+
staffNotes: _staffNotes,
|
|
1708
|
+
...publicData
|
|
1709
|
+
} = data;
|
|
1588
1710
|
ctx.status = 201;
|
|
1589
|
-
ctx.body = { data };
|
|
1711
|
+
ctx.body = { data: publicData };
|
|
1590
1712
|
} catch (err) {
|
|
1591
1713
|
const error = err;
|
|
1592
1714
|
ctx.status = ERROR_STATUS_MAP[error.name] ?? 400;
|
|
@@ -1606,6 +1728,7 @@ const controllers = {
|
|
|
1606
1728
|
inboxController
|
|
1607
1729
|
};
|
|
1608
1730
|
const destroy = ({ strapi }) => {
|
|
1731
|
+
clearSubscribedStrapiInstance(strapi);
|
|
1609
1732
|
strapi.log.debug("[@vicacha-devs/strapi-plugin-inbox] Destroyed");
|
|
1610
1733
|
};
|
|
1611
1734
|
const register = ({ strapi }) => {
|
|
@@ -1613,6 +1736,17 @@ const register = ({ strapi }) => {
|
|
|
1613
1736
|
"[@vicacha-devs/strapi-plugin-inbox] Registered inbox & form builder plugin"
|
|
1614
1737
|
);
|
|
1615
1738
|
};
|
|
1739
|
+
const withPerm = (action) => [
|
|
1740
|
+
"admin::isAuthenticatedAdmin",
|
|
1741
|
+
{
|
|
1742
|
+
name: "admin::hasPermissions",
|
|
1743
|
+
config: { actions: [`plugin::inbox.${action}`] }
|
|
1744
|
+
}
|
|
1745
|
+
];
|
|
1746
|
+
const FORMS_POLICIES = withPerm("forms.manage");
|
|
1747
|
+
const SUBMISSIONS_POLICIES = withPerm("submissions.manage");
|
|
1748
|
+
const SUBSCRIBERS_POLICIES = withPerm("subscribers.manage");
|
|
1749
|
+
const SETTINGS_POLICIES = withPerm("settings.manage");
|
|
1616
1750
|
const admin = {
|
|
1617
1751
|
type: "admin",
|
|
1618
1752
|
routes: [
|
|
@@ -1620,103 +1754,103 @@ const admin = {
|
|
|
1620
1754
|
method: "GET",
|
|
1621
1755
|
path: "/settings",
|
|
1622
1756
|
handler: "inboxController.getSettings",
|
|
1623
|
-
config: { policies:
|
|
1757
|
+
config: { policies: SETTINGS_POLICIES }
|
|
1624
1758
|
},
|
|
1625
1759
|
{
|
|
1626
1760
|
method: "PUT",
|
|
1627
1761
|
path: "/settings",
|
|
1628
1762
|
handler: "inboxController.updateSettings",
|
|
1629
|
-
config: { policies:
|
|
1763
|
+
config: { policies: SETTINGS_POLICIES }
|
|
1630
1764
|
},
|
|
1631
1765
|
{
|
|
1632
1766
|
method: "GET",
|
|
1633
1767
|
path: "/templates",
|
|
1634
1768
|
handler: "inboxController.listTemplates",
|
|
1635
|
-
config: { policies:
|
|
1769
|
+
config: { policies: FORMS_POLICIES }
|
|
1636
1770
|
},
|
|
1637
1771
|
{
|
|
1638
1772
|
method: "GET",
|
|
1639
1773
|
path: "/form-builder-forms",
|
|
1640
1774
|
handler: "inboxController.listFormBuilderForms",
|
|
1641
|
-
config: { policies:
|
|
1775
|
+
config: { policies: FORMS_POLICIES }
|
|
1642
1776
|
},
|
|
1643
1777
|
{
|
|
1644
1778
|
method: "POST",
|
|
1645
1779
|
path: "/forms/sync-builder",
|
|
1646
1780
|
handler: "inboxController.syncFormBuilder",
|
|
1647
|
-
config: { policies:
|
|
1781
|
+
config: { policies: FORMS_POLICIES }
|
|
1648
1782
|
},
|
|
1649
1783
|
{
|
|
1650
1784
|
method: "GET",
|
|
1651
1785
|
path: "/forms",
|
|
1652
1786
|
handler: "inboxController.listForms",
|
|
1653
|
-
config: { policies:
|
|
1787
|
+
config: { policies: FORMS_POLICIES }
|
|
1654
1788
|
},
|
|
1655
1789
|
{
|
|
1656
1790
|
method: "POST",
|
|
1657
1791
|
path: "/forms",
|
|
1658
1792
|
handler: "inboxController.saveForm",
|
|
1659
|
-
config: { policies:
|
|
1793
|
+
config: { policies: FORMS_POLICIES }
|
|
1660
1794
|
},
|
|
1661
1795
|
{
|
|
1662
1796
|
method: "POST",
|
|
1663
1797
|
path: "/forms/from-template",
|
|
1664
1798
|
handler: "inboxController.createFromTemplate",
|
|
1665
|
-
config: { policies:
|
|
1799
|
+
config: { policies: FORMS_POLICIES }
|
|
1666
1800
|
},
|
|
1667
1801
|
{
|
|
1668
1802
|
method: "GET",
|
|
1669
1803
|
path: "/submissions",
|
|
1670
1804
|
handler: "inboxController.listSubmissions",
|
|
1671
|
-
config: { policies:
|
|
1805
|
+
config: { policies: SUBMISSIONS_POLICIES }
|
|
1672
1806
|
},
|
|
1673
1807
|
{
|
|
1674
1808
|
method: "PUT",
|
|
1675
1809
|
path: "/submissions/:documentId/casework",
|
|
1676
1810
|
handler: "inboxController.updateCasework",
|
|
1677
|
-
config: { policies:
|
|
1811
|
+
config: { policies: SUBMISSIONS_POLICIES }
|
|
1678
1812
|
},
|
|
1679
1813
|
{
|
|
1680
1814
|
method: "POST",
|
|
1681
1815
|
path: "/submissions/:documentId/schedule-booking",
|
|
1682
1816
|
handler: "inboxController.scheduleBooking",
|
|
1683
|
-
config: { policies:
|
|
1817
|
+
config: { policies: SUBMISSIONS_POLICIES }
|
|
1684
1818
|
},
|
|
1685
1819
|
{
|
|
1686
1820
|
method: "POST",
|
|
1687
1821
|
path: "/submissions/:documentId/issue-certificate",
|
|
1688
1822
|
handler: "inboxController.issueCertificate",
|
|
1689
|
-
config: { policies:
|
|
1823
|
+
config: { policies: SUBMISSIONS_POLICIES }
|
|
1690
1824
|
},
|
|
1691
1825
|
{
|
|
1692
1826
|
method: "GET",
|
|
1693
1827
|
path: "/submissions/export-csv",
|
|
1694
1828
|
handler: "inboxController.exportCsv",
|
|
1695
|
-
config: { policies:
|
|
1829
|
+
config: { policies: SUBMISSIONS_POLICIES }
|
|
1696
1830
|
},
|
|
1697
1831
|
{
|
|
1698
1832
|
method: "POST",
|
|
1699
1833
|
path: "/captcha/test",
|
|
1700
1834
|
handler: "inboxController.testCaptcha",
|
|
1701
|
-
config: { policies:
|
|
1835
|
+
config: { policies: SETTINGS_POLICIES }
|
|
1702
1836
|
},
|
|
1703
1837
|
{
|
|
1704
1838
|
method: "GET",
|
|
1705
1839
|
path: "/subscribers",
|
|
1706
1840
|
handler: "inboxController.listSubscribers",
|
|
1707
|
-
config: { policies:
|
|
1841
|
+
config: { policies: SUBSCRIBERS_POLICIES }
|
|
1708
1842
|
},
|
|
1709
1843
|
{
|
|
1710
1844
|
method: "GET",
|
|
1711
1845
|
path: "/alerts",
|
|
1712
1846
|
handler: "inboxController.getActiveAlerts",
|
|
1713
|
-
config: { policies:
|
|
1847
|
+
config: { policies: SUBSCRIBERS_POLICIES }
|
|
1714
1848
|
},
|
|
1715
1849
|
{
|
|
1716
1850
|
method: "POST",
|
|
1717
1851
|
path: "/alerts",
|
|
1718
1852
|
handler: "inboxController.upsertAlertBanner",
|
|
1719
|
-
config: { policies:
|
|
1853
|
+
config: { policies: SUBSCRIBERS_POLICIES }
|
|
1720
1854
|
}
|
|
1721
1855
|
]
|
|
1722
1856
|
};
|
|
@@ -1777,6 +1911,15 @@ const contentApi = {
|
|
|
1777
1911
|
auth: false
|
|
1778
1912
|
}
|
|
1779
1913
|
},
|
|
1914
|
+
{
|
|
1915
|
+
method: "GET",
|
|
1916
|
+
path: "/public/unsubscribe",
|
|
1917
|
+
handler: "inboxController.unsubscribe",
|
|
1918
|
+
config: {
|
|
1919
|
+
policies: [],
|
|
1920
|
+
auth: false
|
|
1921
|
+
}
|
|
1922
|
+
},
|
|
1780
1923
|
{
|
|
1781
1924
|
method: "GET",
|
|
1782
1925
|
path: "/alerts",
|
|
@@ -1795,6 +1938,15 @@ const routes = {
|
|
|
1795
1938
|
function getDocuments(strapi, uid) {
|
|
1796
1939
|
return strapi.documents(uid);
|
|
1797
1940
|
}
|
|
1941
|
+
function sanitizeSafeCtaUrl(raw) {
|
|
1942
|
+
if (typeof raw !== "string") return "";
|
|
1943
|
+
const trimmed = raw.trim();
|
|
1944
|
+
if (!trimmed) return "";
|
|
1945
|
+
if (trimmed.startsWith("https://") || trimmed.startsWith("http://") || trimmed.startsWith("/") && !trimmed.startsWith("//")) {
|
|
1946
|
+
return trimmed;
|
|
1947
|
+
}
|
|
1948
|
+
return "";
|
|
1949
|
+
}
|
|
1798
1950
|
function buildAlertBannerPayload(input) {
|
|
1799
1951
|
return {
|
|
1800
1952
|
code: input.code,
|
|
@@ -1802,7 +1954,7 @@ function buildAlertBannerPayload(input) {
|
|
|
1802
1954
|
message: input.message,
|
|
1803
1955
|
severity: input.severity ?? "info",
|
|
1804
1956
|
ctaLabel: input.ctaLabel ?? "",
|
|
1805
|
-
ctaUrl: input.ctaUrl
|
|
1957
|
+
ctaUrl: sanitizeSafeCtaUrl(input.ctaUrl),
|
|
1806
1958
|
dismissible: input.dismissible ?? true,
|
|
1807
1959
|
active: input.active ?? true,
|
|
1808
1960
|
priority: input.priority ?? 10,
|
|
@@ -1816,7 +1968,8 @@ const alertService = ({ strapi }) => ({
|
|
|
1816
1968
|
const settingsService = strapi.plugin(PLUGIN_ID).service("settings");
|
|
1817
1969
|
const settings2 = await settingsService.getSettings();
|
|
1818
1970
|
const locale = params?.locale || settings2.defaultLocale || "es";
|
|
1819
|
-
const
|
|
1971
|
+
const parsedNowMs = new Date(params?.nowIso || Date.now()).getTime();
|
|
1972
|
+
const nowMs = Number.isNaN(parsedNowMs) ? Date.now() : parsedNowMs;
|
|
1820
1973
|
const docs = await getDocuments(strapi, ALERT_BANNER_UID).findMany({
|
|
1821
1974
|
filters: { active: true },
|
|
1822
1975
|
limit: 50
|
|
@@ -1848,7 +2001,7 @@ const alertService = ({ strapi }) => ({
|
|
|
1848
2001
|
title: tr?.title || banner.title,
|
|
1849
2002
|
message: tr?.message || banner.message,
|
|
1850
2003
|
ctaLabel: tr?.ctaLabel || banner.ctaLabel,
|
|
1851
|
-
ctaUrl: tr?.ctaUrl || banner.ctaUrl,
|
|
2004
|
+
ctaUrl: sanitizeSafeCtaUrl(tr?.ctaUrl || banner.ctaUrl),
|
|
1852
2005
|
secondaryDateLabel: secondaryDate.formatted
|
|
1853
2006
|
};
|
|
1854
2007
|
});
|
|
@@ -1862,17 +2015,23 @@ const alertService = ({ strapi }) => ({
|
|
|
1862
2015
|
);
|
|
1863
2016
|
const existing = existingDocs[0] || null;
|
|
1864
2017
|
const payload = buildAlertBannerPayload(input);
|
|
2018
|
+
let saved;
|
|
1865
2019
|
if (existing?.documentId) {
|
|
1866
2020
|
const updated = await getDocuments(strapi, ALERT_BANNER_UID).update({
|
|
1867
2021
|
documentId: existing.documentId,
|
|
1868
2022
|
data: payload
|
|
1869
2023
|
});
|
|
1870
|
-
|
|
2024
|
+
saved = updated;
|
|
2025
|
+
} else {
|
|
2026
|
+
const created = await getDocuments(strapi, ALERT_BANNER_UID).create({
|
|
2027
|
+
data: payload
|
|
2028
|
+
});
|
|
2029
|
+
saved = created;
|
|
1871
2030
|
}
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
}
|
|
1875
|
-
return
|
|
2031
|
+
if (saved.active) {
|
|
2032
|
+
strapi.eventHub?.emit("inbox.alert.published", { entry: saved });
|
|
2033
|
+
}
|
|
2034
|
+
return saved;
|
|
1876
2035
|
}
|
|
1877
2036
|
});
|
|
1878
2037
|
function sanitizePublicForm(form, locale) {
|
|
@@ -2671,18 +2830,20 @@ function assertRequiredChecklistVerified(existing, requireVerified) {
|
|
|
2671
2830
|
async function tryIssuePluginCertificate(strapi, existing, certInput, fallbackCertNumber) {
|
|
2672
2831
|
try {
|
|
2673
2832
|
const certsPlugin = strapi.plugin("certificates");
|
|
2674
|
-
const certSvc = certsPlugin?.service("certificateService");
|
|
2833
|
+
const certSvc = certsPlugin?.service("issuance") ?? certsPlugin?.service("certificateService");
|
|
2675
2834
|
if (!certSvc?.issueCertificate) return fallbackCertNumber;
|
|
2835
|
+
const recipientFullName = certInput?.recipientName || existing.applicantName || "Applicant";
|
|
2676
2836
|
const issued = await certSvc.issueCertificate({
|
|
2677
2837
|
templateSlug: certInput?.templateSlug || "official-service-certificate",
|
|
2678
|
-
recipientName:
|
|
2838
|
+
recipientName: recipientFullName,
|
|
2839
|
+
recipientFullName,
|
|
2679
2840
|
recipientEmail: existing.applicantEmail,
|
|
2680
2841
|
metadata: {
|
|
2681
2842
|
submissionReference: existing.referenceCode,
|
|
2682
2843
|
formSlug: existing.formSlug
|
|
2683
2844
|
}
|
|
2684
2845
|
});
|
|
2685
|
-
return issued?.certificateNumber || fallbackCertNumber;
|
|
2846
|
+
return issued?.serialNumber || issued?.certificateNumber || fallbackCertNumber;
|
|
2686
2847
|
} catch {
|
|
2687
2848
|
return fallbackCertNumber;
|
|
2688
2849
|
}
|
|
@@ -2963,7 +3124,8 @@ function pickSubscriberStr(...values) {
|
|
|
2963
3124
|
function buildSubscriberPayload(email, input, existing, settings2) {
|
|
2964
3125
|
const doubleOptIn = settings2.enableDoubleOptIn !== false;
|
|
2965
3126
|
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
2966
|
-
const
|
|
3127
|
+
const alreadyConfirmed = existing?.status === "confirmed";
|
|
3128
|
+
const status = alreadyConfirmed || !doubleOptIn ? "confirmed" : "pending";
|
|
2967
3129
|
return {
|
|
2968
3130
|
email,
|
|
2969
3131
|
fullName: pickSubscriberStr(input.fullName, existing?.fullName),
|
|
@@ -2971,12 +3133,12 @@ function buildSubscriberPayload(email, input, existing, settings2) {
|
|
|
2971
3133
|
locale: pickSubscriberStr(input.locale, settings2.defaultLocale, "es"),
|
|
2972
3134
|
topics: normalizeSubscriberTopics(input.topics),
|
|
2973
3135
|
status,
|
|
2974
|
-
confirmToken: doubleOptIn ? randomBytes(20).toString("hex")
|
|
3136
|
+
confirmToken: alreadyConfirmed || !doubleOptIn ? null : randomBytes(20).toString("hex"),
|
|
2975
3137
|
unsubscribeToken: pickSubscriberStr(
|
|
2976
3138
|
existing?.unsubscribeToken,
|
|
2977
3139
|
randomBytes(20).toString("hex")
|
|
2978
3140
|
),
|
|
2979
|
-
confirmedAt: doubleOptIn ? null : nowIso,
|
|
3141
|
+
confirmedAt: alreadyConfirmed ? existing.confirmedAt || nowIso : doubleOptIn ? null : nowIso,
|
|
2980
3142
|
unsubscribedAt: null,
|
|
2981
3143
|
sourceFormSlug: pickSubscriberStr(
|
|
2982
3144
|
input.sourceFormSlug,
|