easy-forms-core 1.1.2 → 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.
@@ -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 ["schema", "template", "template-extend", "theme", "colors", "initialData", "loading", "disabled"];
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
  */