easy-forms-core 1.1.3 → 1.1.5
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/easy-form.d.ts +50 -2
- package/dist/easy-form.js +391 -1
- package/dist/easy-form.js.map +1 -1
- package/dist/index.d.ts +117 -3
- package/dist/index.js +395 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/easy-form.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ type FieldType = 'text' | 'email' | 'number' | 'password' | 'textarea' | 'select
|
|
|
5
5
|
/**
|
|
6
6
|
* Tipos de validaciones soportadas
|
|
7
7
|
*/
|
|
8
|
-
type ValidationType = 'required' | 'email' | 'minLength' | 'maxLength' | 'min' | 'max' | 'pattern' | 'custom';
|
|
8
|
+
type ValidationType = 'required' | 'email' | 'minLength' | 'maxLength' | 'min' | 'max' | 'pattern' | 'custom' | 'noInjection';
|
|
9
9
|
/**
|
|
10
10
|
* Operadores de condición
|
|
11
11
|
*/
|
|
@@ -50,10 +50,13 @@ interface CustomValidation extends BaseValidation {
|
|
|
50
50
|
type: 'custom';
|
|
51
51
|
validator: (value: any) => boolean | Promise<boolean>;
|
|
52
52
|
}
|
|
53
|
+
interface NoInjectionValidation extends BaseValidation {
|
|
54
|
+
type: 'noInjection';
|
|
55
|
+
}
|
|
53
56
|
/**
|
|
54
57
|
* Unión de todas las validaciones
|
|
55
58
|
*/
|
|
56
|
-
type Validation = RequiredValidation | EmailValidation | MinLengthValidation | MaxLengthValidation | MinValidation | MaxValidation | PatternValidation | CustomValidation;
|
|
59
|
+
type Validation = RequiredValidation | EmailValidation | MinLengthValidation | MaxLengthValidation | MinValidation | MaxValidation | PatternValidation | CustomValidation | NoInjectionValidation;
|
|
57
60
|
/**
|
|
58
61
|
* Condición individual para campos
|
|
59
62
|
*/
|
|
@@ -109,6 +112,8 @@ interface BaseField {
|
|
|
109
112
|
disabled?: boolean;
|
|
110
113
|
hidden?: boolean;
|
|
111
114
|
description?: string;
|
|
115
|
+
/** Si true, no se aplica validación anti-inyección automática */
|
|
116
|
+
skipInjectionValidation?: boolean;
|
|
112
117
|
dependencies?: FieldDependencies;
|
|
113
118
|
conditionalValidations?: Array<{
|
|
114
119
|
condition: FieldCondition | FieldCondition[];
|
|
@@ -339,6 +344,8 @@ declare class EasyForm extends BrowserHTMLElement {
|
|
|
339
344
|
protected shadow: ShadowRoot;
|
|
340
345
|
private customComponents;
|
|
341
346
|
private isRendering;
|
|
347
|
+
private attemptsLock;
|
|
348
|
+
private lockCountdownInterval;
|
|
342
349
|
static get observedAttributes(): string[];
|
|
343
350
|
constructor();
|
|
344
351
|
/**
|
|
@@ -365,6 +372,21 @@ declare class EasyForm extends BrowserHTMLElement {
|
|
|
365
372
|
* Establece los campos adicionales para extender el template
|
|
366
373
|
*/
|
|
367
374
|
set templateExtend(value: Field[] | null);
|
|
375
|
+
/**
|
|
376
|
+
* Máximo de intentos antes de bloquear (para AttemptsLock)
|
|
377
|
+
*/
|
|
378
|
+
get maxAttempts(): number | null;
|
|
379
|
+
set maxAttempts(value: number | null);
|
|
380
|
+
/**
|
|
381
|
+
* Duración del bloqueo en minutos (default: 5)
|
|
382
|
+
*/
|
|
383
|
+
get blockDurationMinutes(): number | null;
|
|
384
|
+
set blockDurationMinutes(value: number | null);
|
|
385
|
+
/**
|
|
386
|
+
* Clave para persistir intentos en sessionStorage
|
|
387
|
+
*/
|
|
388
|
+
get attemptsStorageKey(): string | null;
|
|
389
|
+
set attemptsStorageKey(value: string | null);
|
|
368
390
|
/**
|
|
369
391
|
* Se llama cuando el componente se conecta al DOM
|
|
370
392
|
*/
|
|
@@ -373,6 +395,10 @@ declare class EasyForm extends BrowserHTMLElement {
|
|
|
373
395
|
* Se llama cuando un atributo cambia
|
|
374
396
|
*/
|
|
375
397
|
attributeChangedCallback(name: string, oldValue: string, newValue: string): void;
|
|
398
|
+
/**
|
|
399
|
+
* Configura el AttemptsLock según los atributos actuales
|
|
400
|
+
*/
|
|
401
|
+
private setupAttemptsLock;
|
|
376
402
|
/**
|
|
377
403
|
* Maneja el cambio de schema
|
|
378
404
|
*/
|
|
@@ -385,6 +411,11 @@ declare class EasyForm extends BrowserHTMLElement {
|
|
|
385
411
|
* Renderiza el formulario
|
|
386
412
|
*/
|
|
387
413
|
private render;
|
|
414
|
+
/**
|
|
415
|
+
* Actualiza el overlay de bloqueo por intentos
|
|
416
|
+
*/
|
|
417
|
+
private updateLockOverlay;
|
|
418
|
+
private stopLockCountdown;
|
|
388
419
|
/**
|
|
389
420
|
* Actualiza el overlay de loading sobre el formulario
|
|
390
421
|
*/
|
|
@@ -467,6 +498,23 @@ declare class EasyForm extends BrowserHTMLElement {
|
|
|
467
498
|
* Resetea el formulario a sus valores iniciales
|
|
468
499
|
*/
|
|
469
500
|
reset(): void;
|
|
501
|
+
/**
|
|
502
|
+
* Incrementa el contador de intentos (para bloqueo por intentos fallidos).
|
|
503
|
+
* El consumidor debe llamar esto cuando la API/login falle.
|
|
504
|
+
*/
|
|
505
|
+
incrementAttempts(): void;
|
|
506
|
+
/**
|
|
507
|
+
* Resetea el contador de intentos y desbloquea el formulario.
|
|
508
|
+
*/
|
|
509
|
+
resetAttempts(): void;
|
|
510
|
+
/**
|
|
511
|
+
* Retorna true si el formulario está bloqueado por intentos.
|
|
512
|
+
*/
|
|
513
|
+
isLocked(): boolean;
|
|
514
|
+
/**
|
|
515
|
+
* Retorna los milisegundos restantes del bloqueo, o 0 si no está bloqueado.
|
|
516
|
+
*/
|
|
517
|
+
getRemainingBlockTimeMs(): number;
|
|
470
518
|
/**
|
|
471
519
|
* Limpia todos los valores del formulario
|
|
472
520
|
*/
|
package/dist/easy-form.js
CHANGED
|
@@ -710,6 +710,28 @@ function getBaseStyles(colors) {
|
|
|
710
710
|
transform: rotate(360deg);
|
|
711
711
|
}
|
|
712
712
|
}
|
|
713
|
+
/* Lock Overlay (bloqueo por intentos) */
|
|
714
|
+
.easy-form-lock-overlay {
|
|
715
|
+
position: absolute;
|
|
716
|
+
top: 0;
|
|
717
|
+
left: 0;
|
|
718
|
+
right: 0;
|
|
719
|
+
bottom: 0;
|
|
720
|
+
background: rgba(255, 255, 255, 0.9);
|
|
721
|
+
display: flex;
|
|
722
|
+
align-items: center;
|
|
723
|
+
justify-content: center;
|
|
724
|
+
z-index: 1001;
|
|
725
|
+
backdrop-filter: blur(4px);
|
|
726
|
+
border-radius: inherit;
|
|
727
|
+
text-align: center;
|
|
728
|
+
padding: 1.5rem;
|
|
729
|
+
}
|
|
730
|
+
.easy-form-lock-message {
|
|
731
|
+
font-size: 1rem;
|
|
732
|
+
color: var(--easy-form-text);
|
|
733
|
+
max-width: 280px;
|
|
734
|
+
}
|
|
713
735
|
/* Disabled State */
|
|
714
736
|
.easy-form-disabled,
|
|
715
737
|
.easy-form-disabled * {
|
|
@@ -1635,6 +1657,191 @@ function getPredefinedMask(type) {
|
|
|
1635
1657
|
return PREDEFINED_MASKS[type];
|
|
1636
1658
|
}
|
|
1637
1659
|
|
|
1660
|
+
// src/utils/injection-validation.ts
|
|
1661
|
+
var INJECTION_VALIDATION_MESSAGE = "El valor contiene caracteres o patrones no permitidos";
|
|
1662
|
+
var INJECTION_PATTERNS = [
|
|
1663
|
+
// SQL Injection
|
|
1664
|
+
/\b(union|select|insert|update|delete|drop|exec|execute|declare)\s+(all\s+)?(select|from|into|table)/i,
|
|
1665
|
+
/\b(or|and)\s+['"]?\d+['"]?\s*=\s*['"]?\d+/i,
|
|
1666
|
+
/;\s*(drop|delete|truncate|alter)\s+/i,
|
|
1667
|
+
/--\s*$/,
|
|
1668
|
+
// SQL comment
|
|
1669
|
+
/\/\*[\s\S]*\*\//,
|
|
1670
|
+
// Block comment
|
|
1671
|
+
/'\s*or\s+'1'\s*=\s*'1/i,
|
|
1672
|
+
/"\s*or\s+"1"\s*=\s*"1/i,
|
|
1673
|
+
/\bexec\s*\(/i,
|
|
1674
|
+
/\bxp_\w+/i,
|
|
1675
|
+
// SQL Server extended procedures
|
|
1676
|
+
// XSS / Scripting
|
|
1677
|
+
/<script\b[\s\S]*?>[\s\S]*?<\/script>/i,
|
|
1678
|
+
/<script\b/i,
|
|
1679
|
+
/javascript\s*:/i,
|
|
1680
|
+
/vbscript\s*:/i,
|
|
1681
|
+
/on\w+\s*=\s*["'][^"']*["']/i,
|
|
1682
|
+
// onclick=, onerror=, onload=, etc.
|
|
1683
|
+
/on\w+\s*=\s*[^\s>]+/i,
|
|
1684
|
+
/<iframe\b/i,
|
|
1685
|
+
/<object\b/i,
|
|
1686
|
+
/<embed\b/i,
|
|
1687
|
+
/\beval\s*\(/i,
|
|
1688
|
+
/document\.(cookie|write|location)/i,
|
|
1689
|
+
/window\.(location|open|eval)/i,
|
|
1690
|
+
// Command injection (shell)
|
|
1691
|
+
/[;&|]\s*(ls|cat|rm|wget|curl|nc|bash|sh|python|perl)\s/i,
|
|
1692
|
+
/\$\s*\([^)]+\)/,
|
|
1693
|
+
// $(...)
|
|
1694
|
+
/`[^`]+`/,
|
|
1695
|
+
// Backtick command substitution
|
|
1696
|
+
/\|\s*\w+/,
|
|
1697
|
+
// Pipe to command (with word after)
|
|
1698
|
+
// NoSQL / Template injection
|
|
1699
|
+
/\$\s*where\b/i,
|
|
1700
|
+
/\$\s*gt\b|\$\s*ne\b|\$\s*regex\b/i,
|
|
1701
|
+
/\{\{[^}]*\}\}/,
|
|
1702
|
+
// Template literals
|
|
1703
|
+
/\$\{[^}]*\}/
|
|
1704
|
+
// JS template literals
|
|
1705
|
+
];
|
|
1706
|
+
function containsInjection(value) {
|
|
1707
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
1708
|
+
return false;
|
|
1709
|
+
}
|
|
1710
|
+
const normalized = value.trim();
|
|
1711
|
+
if (normalized.length === 0) return false;
|
|
1712
|
+
for (const pattern of INJECTION_PATTERNS) {
|
|
1713
|
+
if (pattern.test(value)) {
|
|
1714
|
+
return true;
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
return false;
|
|
1718
|
+
}
|
|
1719
|
+
function isSafeFromInjection(value) {
|
|
1720
|
+
if (value === null || value === void 0) {
|
|
1721
|
+
return true;
|
|
1722
|
+
}
|
|
1723
|
+
if (typeof value === "string") {
|
|
1724
|
+
return !containsInjection(value);
|
|
1725
|
+
}
|
|
1726
|
+
if (Array.isArray(value)) {
|
|
1727
|
+
return value.every((item) => isSafeFromInjection(item));
|
|
1728
|
+
}
|
|
1729
|
+
if (typeof value === "object") {
|
|
1730
|
+
return Object.values(value).every(
|
|
1731
|
+
(v) => isSafeFromInjection(v)
|
|
1732
|
+
);
|
|
1733
|
+
}
|
|
1734
|
+
return true;
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
// src/utils/attempts-lock.ts
|
|
1738
|
+
var AttemptsLock = class {
|
|
1739
|
+
constructor(options) {
|
|
1740
|
+
this.attempts = 0;
|
|
1741
|
+
this.lockedUntil = null;
|
|
1742
|
+
this.unlockCheckInterval = null;
|
|
1743
|
+
this.maxAttempts = Math.max(1, options.maxAttempts);
|
|
1744
|
+
this.blockDurationMs = (options.blockDurationMinutes ?? 5) * 60 * 1e3;
|
|
1745
|
+
this.storageKey = options.storageKey;
|
|
1746
|
+
this.onLocked = options.onLocked;
|
|
1747
|
+
this.onUnlocked = options.onUnlocked;
|
|
1748
|
+
if (this.storageKey && typeof sessionStorage !== "undefined") {
|
|
1749
|
+
this.loadFromStorage();
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
loadFromStorage() {
|
|
1753
|
+
if (!this.storageKey || typeof sessionStorage === "undefined") return;
|
|
1754
|
+
try {
|
|
1755
|
+
const stored = sessionStorage.getItem(this.storageKey);
|
|
1756
|
+
if (stored) {
|
|
1757
|
+
const data = JSON.parse(stored);
|
|
1758
|
+
this.attempts = data.attempts ?? 0;
|
|
1759
|
+
this.lockedUntil = data.lockedUntil ?? null;
|
|
1760
|
+
this.checkExpiration();
|
|
1761
|
+
}
|
|
1762
|
+
} catch {
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
saveToStorage() {
|
|
1766
|
+
if (!this.storageKey || typeof sessionStorage === "undefined") return;
|
|
1767
|
+
try {
|
|
1768
|
+
const data = {
|
|
1769
|
+
attempts: this.attempts,
|
|
1770
|
+
lockedUntil: this.lockedUntil ?? void 0
|
|
1771
|
+
};
|
|
1772
|
+
sessionStorage.setItem(this.storageKey, JSON.stringify(data));
|
|
1773
|
+
} catch {
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
checkExpiration() {
|
|
1777
|
+
if (this.lockedUntil === null) return false;
|
|
1778
|
+
if (Date.now() >= this.lockedUntil) {
|
|
1779
|
+
this.reset();
|
|
1780
|
+
this.onUnlocked?.();
|
|
1781
|
+
return true;
|
|
1782
|
+
}
|
|
1783
|
+
return false;
|
|
1784
|
+
}
|
|
1785
|
+
startUnlockCheck() {
|
|
1786
|
+
if (this.unlockCheckInterval) return;
|
|
1787
|
+
this.unlockCheckInterval = setInterval(() => {
|
|
1788
|
+
if (this.checkExpiration()) {
|
|
1789
|
+
this.stopUnlockCheck();
|
|
1790
|
+
}
|
|
1791
|
+
}, 1e3);
|
|
1792
|
+
}
|
|
1793
|
+
stopUnlockCheck() {
|
|
1794
|
+
if (this.unlockCheckInterval) {
|
|
1795
|
+
clearInterval(this.unlockCheckInterval);
|
|
1796
|
+
this.unlockCheckInterval = null;
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
/**
|
|
1800
|
+
* Incrementa el contador de intentos. Si alcanza maxAttempts, bloquea.
|
|
1801
|
+
*/
|
|
1802
|
+
incrementAttempts() {
|
|
1803
|
+
this.attempts++;
|
|
1804
|
+
if (this.attempts >= this.maxAttempts) {
|
|
1805
|
+
this.lockedUntil = Date.now() + this.blockDurationMs;
|
|
1806
|
+
this.saveToStorage();
|
|
1807
|
+
this.onLocked?.(this.blockDurationMs);
|
|
1808
|
+
this.startUnlockCheck();
|
|
1809
|
+
} else {
|
|
1810
|
+
this.saveToStorage();
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
/**
|
|
1814
|
+
* Retorna true si el lock está activo (aún dentro del período de bloqueo)
|
|
1815
|
+
*/
|
|
1816
|
+
isLocked() {
|
|
1817
|
+
if (this.checkExpiration()) return false;
|
|
1818
|
+
return this.lockedUntil !== null && Date.now() < this.lockedUntil;
|
|
1819
|
+
}
|
|
1820
|
+
/**
|
|
1821
|
+
* Retorna los milisegundos restantes del bloqueo, o 0 si no está bloqueado
|
|
1822
|
+
*/
|
|
1823
|
+
getRemainingBlockTimeMs() {
|
|
1824
|
+
if (this.checkExpiration()) return 0;
|
|
1825
|
+
if (this.lockedUntil === null) return 0;
|
|
1826
|
+
return Math.max(0, this.lockedUntil - Date.now());
|
|
1827
|
+
}
|
|
1828
|
+
/**
|
|
1829
|
+
* Resetea el contador de intentos y el bloqueo
|
|
1830
|
+
*/
|
|
1831
|
+
reset() {
|
|
1832
|
+
this.attempts = 0;
|
|
1833
|
+
this.lockedUntil = null;
|
|
1834
|
+
this.stopUnlockCheck();
|
|
1835
|
+
this.saveToStorage();
|
|
1836
|
+
}
|
|
1837
|
+
/**
|
|
1838
|
+
* Retorna el número actual de intentos
|
|
1839
|
+
*/
|
|
1840
|
+
getAttempts() {
|
|
1841
|
+
return this.attempts;
|
|
1842
|
+
}
|
|
1843
|
+
};
|
|
1844
|
+
|
|
1638
1845
|
// src/utils/index.ts
|
|
1639
1846
|
function attributeValue(value) {
|
|
1640
1847
|
if (value === null || value === void 0) {
|
|
@@ -1722,6 +1929,8 @@ var ValidationEngine = class {
|
|
|
1722
1929
|
return this.validatePattern(value, validation.value);
|
|
1723
1930
|
case "custom":
|
|
1724
1931
|
return await this.validateCustom(value, validation);
|
|
1932
|
+
case "noInjection":
|
|
1933
|
+
return this.validateNoInjection(value, validation);
|
|
1725
1934
|
default:
|
|
1726
1935
|
return { isValid: true };
|
|
1727
1936
|
}
|
|
@@ -1832,6 +2041,19 @@ var ValidationEngine = class {
|
|
|
1832
2041
|
message: isValid ? void 0 : "El formato no es v\xE1lido"
|
|
1833
2042
|
};
|
|
1834
2043
|
}
|
|
2044
|
+
/**
|
|
2045
|
+
* Valida que el valor no contenga patrones de inyección (SQL, XSS, etc.)
|
|
2046
|
+
*/
|
|
2047
|
+
validateNoInjection(value, validation) {
|
|
2048
|
+
if (value === null || value === void 0 || value === "") {
|
|
2049
|
+
return { isValid: true };
|
|
2050
|
+
}
|
|
2051
|
+
const isValid = isSafeFromInjection(value);
|
|
2052
|
+
return {
|
|
2053
|
+
isValid,
|
|
2054
|
+
message: isValid ? void 0 : validation.message || INJECTION_VALIDATION_MESSAGE
|
|
2055
|
+
};
|
|
2056
|
+
}
|
|
1835
2057
|
/**
|
|
1836
2058
|
* Valida con función personalizada
|
|
1837
2059
|
*/
|
|
@@ -1870,6 +2092,8 @@ var ValidationEngine = class {
|
|
|
1870
2092
|
return "El formato no es v\xE1lido";
|
|
1871
2093
|
case "custom":
|
|
1872
2094
|
return "Validaci\xF3n fallida";
|
|
2095
|
+
case "noInjection":
|
|
2096
|
+
return INJECTION_VALIDATION_MESSAGE;
|
|
1873
2097
|
default:
|
|
1874
2098
|
return "Campo inv\xE1lido";
|
|
1875
2099
|
}
|
|
@@ -2348,6 +2572,11 @@ var StateManager = class {
|
|
|
2348
2572
|
*/
|
|
2349
2573
|
getActiveValidations(field) {
|
|
2350
2574
|
let validations = [...field.validations || []];
|
|
2575
|
+
const textFieldTypes = ["text", "email", "password", "textarea"];
|
|
2576
|
+
const skipInjection = field.skipInjectionValidation ?? field.props?.skipInjectionValidation;
|
|
2577
|
+
if (textFieldTypes.includes(field.type) && !skipInjection && !validations.some((v) => v.type === "noInjection")) {
|
|
2578
|
+
validations = [{ type: "noInjection" }, ...validations];
|
|
2579
|
+
}
|
|
2351
2580
|
const isRequired = this.getFieldRequired(field.name);
|
|
2352
2581
|
const hasRequiredValidation = validations.some((v) => v.type === "required");
|
|
2353
2582
|
if (isRequired && !hasRequiredValidation) {
|
|
@@ -4525,12 +4754,26 @@ var EasyForm = class extends BrowserHTMLElement {
|
|
|
4525
4754
|
super();
|
|
4526
4755
|
this.customComponents = {};
|
|
4527
4756
|
this.isRendering = false;
|
|
4757
|
+
this.attemptsLock = null;
|
|
4758
|
+
this.lockCountdownInterval = null;
|
|
4528
4759
|
this.dependencyRenderTimeout = null;
|
|
4529
4760
|
this.stateManager = new StateManager();
|
|
4530
4761
|
this.shadow = this.attachShadow({ mode: "open" });
|
|
4531
4762
|
}
|
|
4532
4763
|
static get observedAttributes() {
|
|
4533
|
-
return [
|
|
4764
|
+
return [
|
|
4765
|
+
"schema",
|
|
4766
|
+
"template",
|
|
4767
|
+
"template-extend",
|
|
4768
|
+
"theme",
|
|
4769
|
+
"colors",
|
|
4770
|
+
"initialData",
|
|
4771
|
+
"loading",
|
|
4772
|
+
"disabled",
|
|
4773
|
+
"max-attempts",
|
|
4774
|
+
"block-duration-minutes",
|
|
4775
|
+
"attempts-storage-key"
|
|
4776
|
+
];
|
|
4534
4777
|
}
|
|
4535
4778
|
/**
|
|
4536
4779
|
* Obtiene el schema
|
|
@@ -4604,10 +4847,56 @@ var EasyForm = class extends BrowserHTMLElement {
|
|
|
4604
4847
|
this.removeAttribute("template-extend");
|
|
4605
4848
|
}
|
|
4606
4849
|
}
|
|
4850
|
+
/**
|
|
4851
|
+
* Máximo de intentos antes de bloquear (para AttemptsLock)
|
|
4852
|
+
*/
|
|
4853
|
+
get maxAttempts() {
|
|
4854
|
+
const attr = this.getAttribute("max-attempts");
|
|
4855
|
+
if (!attr) return null;
|
|
4856
|
+
const n = parseInt(attr, 10);
|
|
4857
|
+
return isNaN(n) ? null : n;
|
|
4858
|
+
}
|
|
4859
|
+
set maxAttempts(value) {
|
|
4860
|
+
if (value != null && value >= 1) {
|
|
4861
|
+
this.setAttribute("max-attempts", String(value));
|
|
4862
|
+
} else {
|
|
4863
|
+
this.removeAttribute("max-attempts");
|
|
4864
|
+
}
|
|
4865
|
+
}
|
|
4866
|
+
/**
|
|
4867
|
+
* Duración del bloqueo en minutos (default: 5)
|
|
4868
|
+
*/
|
|
4869
|
+
get blockDurationMinutes() {
|
|
4870
|
+
const attr = this.getAttribute("block-duration-minutes");
|
|
4871
|
+
if (!attr) return null;
|
|
4872
|
+
const n = parseInt(attr, 10);
|
|
4873
|
+
return isNaN(n) ? null : n;
|
|
4874
|
+
}
|
|
4875
|
+
set blockDurationMinutes(value) {
|
|
4876
|
+
if (value != null && value >= 1) {
|
|
4877
|
+
this.setAttribute("block-duration-minutes", String(value));
|
|
4878
|
+
} else {
|
|
4879
|
+
this.removeAttribute("block-duration-minutes");
|
|
4880
|
+
}
|
|
4881
|
+
}
|
|
4882
|
+
/**
|
|
4883
|
+
* Clave para persistir intentos en sessionStorage
|
|
4884
|
+
*/
|
|
4885
|
+
get attemptsStorageKey() {
|
|
4886
|
+
return this.getAttribute("attempts-storage-key");
|
|
4887
|
+
}
|
|
4888
|
+
set attemptsStorageKey(value) {
|
|
4889
|
+
if (value) {
|
|
4890
|
+
this.setAttribute("attempts-storage-key", value);
|
|
4891
|
+
} else {
|
|
4892
|
+
this.removeAttribute("attempts-storage-key");
|
|
4893
|
+
}
|
|
4894
|
+
}
|
|
4607
4895
|
/**
|
|
4608
4896
|
* Se llama cuando el componente se conecta al DOM
|
|
4609
4897
|
*/
|
|
4610
4898
|
connectedCallback() {
|
|
4899
|
+
this.setupAttemptsLock();
|
|
4611
4900
|
this.setupStyles();
|
|
4612
4901
|
this.render();
|
|
4613
4902
|
}
|
|
@@ -4654,6 +4943,33 @@ var EasyForm = class extends BrowserHTMLElement {
|
|
|
4654
4943
|
if (name === "disabled" && newValue !== oldValue) {
|
|
4655
4944
|
this.render();
|
|
4656
4945
|
}
|
|
4946
|
+
if ((name === "max-attempts" || name === "block-duration-minutes" || name === "attempts-storage-key") && newValue !== oldValue) {
|
|
4947
|
+
this.setupAttemptsLock();
|
|
4948
|
+
this.updateLockOverlay();
|
|
4949
|
+
}
|
|
4950
|
+
}
|
|
4951
|
+
/**
|
|
4952
|
+
* Configura el AttemptsLock según los atributos actuales
|
|
4953
|
+
*/
|
|
4954
|
+
setupAttemptsLock() {
|
|
4955
|
+
const maxAttempts = this.maxAttempts;
|
|
4956
|
+
if (maxAttempts == null || maxAttempts < 1) {
|
|
4957
|
+
this.attemptsLock = null;
|
|
4958
|
+
return;
|
|
4959
|
+
}
|
|
4960
|
+
this.attemptsLock = new AttemptsLock({
|
|
4961
|
+
maxAttempts,
|
|
4962
|
+
blockDurationMinutes: this.blockDurationMinutes ?? 5,
|
|
4963
|
+
storageKey: this.attemptsStorageKey ?? void 0,
|
|
4964
|
+
onLocked: () => {
|
|
4965
|
+
this.updateLockOverlay();
|
|
4966
|
+
},
|
|
4967
|
+
onUnlocked: () => {
|
|
4968
|
+
this.stopLockCountdown();
|
|
4969
|
+
this.updateLockOverlay();
|
|
4970
|
+
this.render();
|
|
4971
|
+
}
|
|
4972
|
+
});
|
|
4657
4973
|
}
|
|
4658
4974
|
/**
|
|
4659
4975
|
* Maneja el cambio de schema
|
|
@@ -4768,10 +5084,50 @@ var EasyForm = class extends BrowserHTMLElement {
|
|
|
4768
5084
|
if (this.loading) {
|
|
4769
5085
|
this.updateLoadingOverlay(newFormElement);
|
|
4770
5086
|
}
|
|
5087
|
+
this.updateLockOverlay(newFormElement);
|
|
4771
5088
|
} finally {
|
|
4772
5089
|
this.isRendering = false;
|
|
4773
5090
|
}
|
|
4774
5091
|
}
|
|
5092
|
+
/**
|
|
5093
|
+
* Actualiza el overlay de bloqueo por intentos
|
|
5094
|
+
*/
|
|
5095
|
+
updateLockOverlay(formElement) {
|
|
5096
|
+
const existingOverlay = this.shadow.querySelector(".easy-form-lock-overlay");
|
|
5097
|
+
if (existingOverlay) {
|
|
5098
|
+
existingOverlay.remove();
|
|
5099
|
+
}
|
|
5100
|
+
this.stopLockCountdown();
|
|
5101
|
+
if (!this.attemptsLock?.isLocked()) return;
|
|
5102
|
+
const form = formElement || this.shadow.querySelector("form");
|
|
5103
|
+
if (!form) return;
|
|
5104
|
+
const overlay = document.createElement("div");
|
|
5105
|
+
overlay.className = "easy-form-lock-overlay";
|
|
5106
|
+
const message = document.createElement("div");
|
|
5107
|
+
message.className = "easy-form-lock-message";
|
|
5108
|
+
message.setAttribute("role", "alert");
|
|
5109
|
+
const updateCountdown = () => {
|
|
5110
|
+
const remainingMs = this.attemptsLock.getRemainingBlockTimeMs();
|
|
5111
|
+
if (remainingMs <= 0) {
|
|
5112
|
+
this.stopLockCountdown();
|
|
5113
|
+
return;
|
|
5114
|
+
}
|
|
5115
|
+
const minutes = Math.floor(remainingMs / 6e4);
|
|
5116
|
+
const seconds = Math.floor(remainingMs % 6e4 / 1e3);
|
|
5117
|
+
const timeStr = minutes > 0 ? `${minutes} min ${seconds} s` : `${seconds} segundos`;
|
|
5118
|
+
message.textContent = `Demasiados intentos. Intenta de nuevo en ${timeStr}.`;
|
|
5119
|
+
};
|
|
5120
|
+
updateCountdown();
|
|
5121
|
+
overlay.appendChild(message);
|
|
5122
|
+
form.appendChild(overlay);
|
|
5123
|
+
this.lockCountdownInterval = setInterval(updateCountdown, 1e3);
|
|
5124
|
+
}
|
|
5125
|
+
stopLockCountdown() {
|
|
5126
|
+
if (this.lockCountdownInterval) {
|
|
5127
|
+
clearInterval(this.lockCountdownInterval);
|
|
5128
|
+
this.lockCountdownInterval = null;
|
|
5129
|
+
}
|
|
5130
|
+
}
|
|
4775
5131
|
/**
|
|
4776
5132
|
* Actualiza el overlay de loading sobre el formulario
|
|
4777
5133
|
*/
|
|
@@ -5328,6 +5684,9 @@ var EasyForm = class extends BrowserHTMLElement {
|
|
|
5328
5684
|
*/
|
|
5329
5685
|
async handleSubmit(event) {
|
|
5330
5686
|
event.preventDefault();
|
|
5687
|
+
if (this.attemptsLock?.isLocked()) {
|
|
5688
|
+
return;
|
|
5689
|
+
}
|
|
5331
5690
|
const errors = await this.stateManager.validateForm();
|
|
5332
5691
|
const state = this.stateManager.getState();
|
|
5333
5692
|
if (Object.keys(errors).length > 0) {
|
|
@@ -5393,6 +5752,37 @@ var EasyForm = class extends BrowserHTMLElement {
|
|
|
5393
5752
|
this.stateManager.reset();
|
|
5394
5753
|
this.render();
|
|
5395
5754
|
}
|
|
5755
|
+
/**
|
|
5756
|
+
* Incrementa el contador de intentos (para bloqueo por intentos fallidos).
|
|
5757
|
+
* El consumidor debe llamar esto cuando la API/login falle.
|
|
5758
|
+
*/
|
|
5759
|
+
incrementAttempts() {
|
|
5760
|
+
this.attemptsLock?.incrementAttempts();
|
|
5761
|
+
if (this.attemptsLock?.isLocked()) {
|
|
5762
|
+
this.updateLockOverlay();
|
|
5763
|
+
}
|
|
5764
|
+
}
|
|
5765
|
+
/**
|
|
5766
|
+
* Resetea el contador de intentos y desbloquea el formulario.
|
|
5767
|
+
*/
|
|
5768
|
+
resetAttempts() {
|
|
5769
|
+
this.attemptsLock?.reset();
|
|
5770
|
+
this.stopLockCountdown();
|
|
5771
|
+
this.updateLockOverlay();
|
|
5772
|
+
this.render();
|
|
5773
|
+
}
|
|
5774
|
+
/**
|
|
5775
|
+
* Retorna true si el formulario está bloqueado por intentos.
|
|
5776
|
+
*/
|
|
5777
|
+
isLocked() {
|
|
5778
|
+
return this.attemptsLock?.isLocked() ?? false;
|
|
5779
|
+
}
|
|
5780
|
+
/**
|
|
5781
|
+
* Retorna los milisegundos restantes del bloqueo, o 0 si no está bloqueado.
|
|
5782
|
+
*/
|
|
5783
|
+
getRemainingBlockTimeMs() {
|
|
5784
|
+
return this.attemptsLock?.getRemainingBlockTimeMs() ?? 0;
|
|
5785
|
+
}
|
|
5396
5786
|
/**
|
|
5397
5787
|
* Limpia todos los valores del formulario
|
|
5398
5788
|
*/
|