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