pin_forgot.html 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. <!DOCTYPE html>
  2. <html lang="es">
  3. <head>
  4. <meta charset="UTF-8" />
  5. <title>Recuperar PIN - Biergarten Klein</title>
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7. <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
  8. <meta http-equiv="Pragma" content="no-cache">
  9. <meta http-equiv="Expires" content="0">
  10. <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  11. <link rel="stylesheet" href="https://fonts.googleapis.com/css2?display=swap&family=Noto+Sans:wght@400;500;700;900&family=Spline+Sans:wght@400;500;700">
  12. <script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
  13. <script>
  14. tailwind.config = {
  15. theme: {
  16. extend: {
  17. colors: {
  18. 'custom-dark': '#101419',
  19. 'custom-dark-hover': '#37404a',
  20. 'gray-50': '#f9fafb',
  21. 'gray-100': '#f3f4f6',
  22. }
  23. }
  24. }
  25. }
  26. </script>
  27. </head>
  28. <body class="min-h-screen bg-gray-50 flex items-center justify-center p-4 font-sans" style='font-family:"Spline Sans","Noto Sans",sans-serif;'>
  29. <div class="w-full max-w-md">
  30. <div class="text-center mb-8">
  31. <div class="inline-flex items-center justify-center w-16 h-16 bg-[#101419] rounded-full mb-4">
  32. <svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  33. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
  34. </svg>
  35. </div>
  36. <h1 class="text-[26px] font-bold text-[#101419] tracking-tight mb-2">¿Olvidaste tu PIN?</h1>
  37. <p class="text-[#58728d] text-sm leading-relaxed">No te preocupes, te ayudamos a recuperar el acceso a tu cuenta</p>
  38. </div>
  39. <div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 mb-6">
  40. <div class="space-y-4">
  41. <div class="flex items-start space-x-3" id="step1Indicator">
  42. <div class="step-circle flex-shrink-0 w-6 h-6 bg-[#101419] text-white rounded-full flex items-center justify-center text-xs font-medium">1</div>
  43. <div class="text-sm">
  44. <p class="step-title font-medium text-[#101419]">Ingresa tu correo</p>
  45. <p class="step-desc text-[#58728d] mt-1">Te enviaremos un código</p>
  46. </div>
  47. </div>
  48. <div class="flex items-start space-x-3" id="step2Indicator">
  49. <div class="step-circle flex-shrink-0 w-6 h-6 bg-gray-300 text-gray-600 rounded-full flex items-center justify-center text-xs font-medium">2</div>
  50. <div class="text-sm">
  51. <p class="step-title font-medium text-gray-400">Código de verificación</p>
  52. <p class="step-desc text-gray-400 mt-1">Ingresa el código de 6 dígitos</p>
  53. </div>
  54. </div>
  55. <div class="flex items-start space-x-3" id="step3Indicator">
  56. <div class="step-circle flex-shrink-0 w-6 h-6 bg-gray-300 text-gray-600 rounded-full flex items-center justify-center text-xs font-medium">3</div>
  57. <div class="text-sm">
  58. <p class="step-title font-medium text-gray-400">Nuevo PIN</p>
  59. <p class="step-desc text-gray-400 mt-1">Crea tu nuevo PIN</p>
  60. </div>
  61. </div>
  62. </div>
  63. </div>
  64. <form id="emailForm" class="bg-white rounded-xl shadow-sm border border-gray-200 p-8 space-y-6">
  65. <div class="text-center">
  66. <h2 class="text-[19px] font-bold text-[#101419] mb-2">Ingresa tu correo</h2>
  67. <p class="text-sm text-[#58728d]">Te enviaremos un código de verificación</p>
  68. </div>
  69. <div>
  70. <label for="emailInput" class="block text-sm font-medium text-[#101419] mb-2">Correo electrónico</label>
  71. <input id="emailInput" name="email" type="email" class="w-full border border-gray-300 px-4 py-3 rounded-lg focus:ring-2 focus:ring-[#101419] outline-none" placeholder="tu@email.com" required />
  72. </div>
  73. <div class="space-y-3">
  74. <button id="emailSubmitBtn" type="submit" class="w-full bg-[#101419] hover:bg-[#37404a] text-white py-3 rounded-lg font-medium disabled:opacity-50 transition-colors">Enviar código</button>
  75. <a href="/" class="block w-full text-center border border-gray-300 text-[#101419] py-3 rounded-lg font-medium hover:border-[#101419] transition-colors">Volver al inicio</a>
  76. </div>
  77. </form>
  78. <form id="codeForm" class="bg-white rounded-xl shadow-sm border border-gray-200 p-8 space-y-6 hidden">
  79. <div class="text-center">
  80. <h2 class="text-[19px] font-bold text-[#101419] mb-2">Código de verificación</h2>
  81. <p class="text-sm text-[#58728d]">Ingresa el código enviado a <span id="emailDisplay" class="font-medium"></span></p>
  82. </div>
  83. <div>
  84. <label for="codeInput" class="block text-sm font-medium text-[#101419] mb-2">Código (6 dígitos)</label>
  85. <input id="codeInput" name="code" type="text" maxlength="6" inputmode="numeric" class="w-full border border-gray-300 px-4 py-3 rounded-lg focus:ring-2 focus:ring-[#101419] outline-none text-center text-2xl tracking-widest" placeholder="000000" required />
  86. </div>
  87. <div class="text-center">
  88. <button type="button" id="resendCodeBtn" class="text-sm text-[#58728d] hover:text-[#101419] disabled:opacity-50 transition-colors">
  89. ¿No recibiste el código? <span class="font-medium" id="resendText">Reenviar</span>
  90. </button>
  91. </div>
  92. <div class="space-y-3">
  93. <button id="codeSubmitBtn" type="submit" class="w-full bg-[#101419] hover:bg-[#37404a] text-white py-3 rounded-lg font-medium disabled:opacity-50 transition-colors">Verificar código</button>
  94. <button type="button" id="backToEmailBtn" class="block w-full text-center border border-gray-300 text-[#101419] py-3 rounded-lg font-medium hover:border-[#101419] transition-colors">Cambiar correo</button>
  95. </div>
  96. </form>
  97. <form id="pinForm" class="bg-white rounded-xl shadow-sm border border-gray-200 p-8 space-y-6 hidden">
  98. <div class="text-center">
  99. <h2 class="text-[19px] font-bold text-[#101419] mb-2">Crea tu nuevo PIN</h2>
  100. <p class="text-sm text-[#58728d]">Ingresa un PIN de 4 dígitos</p>
  101. </div>
  102. <div class="space-y-4">
  103. <div>
  104. <label for="newPinInput" class="block text-sm font-medium text-[#101419] mb-2">Nuevo PIN</label>
  105. <input id="newPinInput" name="newPin" type="password" maxlength="4" inputmode="numeric" class="w-full border border-gray-300 px-4 py-3 rounded-lg focus:ring-2 focus:ring-[#101419] outline-none text-center text-2xl tracking-widest" placeholder="••••" required />
  106. </div>
  107. <div>
  108. <label for="confirmPinInput" class="block text-sm font-medium text-[#101419] mb-2">Confirmar PIN</label>
  109. <input id="confirmPinInput" name="confirmPin" type="password" maxlength="4" inputmode="numeric" class="w-full border border-gray-300 px-4 py-3 rounded-lg focus:ring-2 focus:ring-[#101419] outline-none text-center text-2xl tracking-widest" placeholder="••••" required />
  110. </div>
  111. <div class="bg-blue-50 border border-blue-200 text-blue-800 px-4 py-3 rounded-lg text-sm flex items-start space-x-2">
  112. <svg class="w-4 h-4 mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/></svg>
  113. <p>Elige un PIN seguro pero fácil de recordar.</p>
  114. </div>
  115. </div>
  116. <button id="pinSubmitBtn" type="submit" class="w-full bg-[#101419] hover:bg-[#37404a] text-white py-3 rounded-lg font-medium disabled:opacity-50 transition-colors">Establecer PIN</button>
  117. </form>
  118. <div id="errorMessage" class="hidden bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg text-sm mt-4 text-center"></div>
  119. <div id="successMessage" class="hidden bg-green-50 border border-green-200 text-green-700 px-4 py-3 rounded-lg text-sm mt-4 text-center"></div>
  120. </div>
  121. <script>
  122. // --- ESTADO GLOBAL ---
  123. const state = {
  124. step: 1,
  125. email: '',
  126. token: '',
  127. resendTimer: null,
  128. resendCountdown: 0
  129. };
  130. // --- REFERENCIAS DOM ---
  131. const dom = {
  132. forms: {
  133. email: document.getElementById('emailForm'),
  134. code: document.getElementById('codeForm'),
  135. pin: document.getElementById('pinForm')
  136. },
  137. inputs: {
  138. email: document.getElementById('emailInput'),
  139. code: document.getElementById('codeInput'),
  140. newPin: document.getElementById('newPinInput'),
  141. confirmPin: document.getElementById('confirmPinInput')
  142. },
  143. buttons: {
  144. email: document.getElementById('emailSubmitBtn'),
  145. code: document.getElementById('codeSubmitBtn'),
  146. pin: document.getElementById('pinSubmitBtn'),
  147. resend: document.getElementById('resendCodeBtn'),
  148. resendText: document.getElementById('resendText'),
  149. back: document.getElementById('backToEmailBtn')
  150. },
  151. messages: {
  152. error: document.getElementById('errorMessage'),
  153. success: document.getElementById('successMessage'),
  154. emailDisplay: document.getElementById('emailDisplay')
  155. },
  156. indicators: [
  157. document.getElementById('step1Indicator'),
  158. document.getElementById('step2Indicator'),
  159. document.getElementById('step3Indicator')
  160. ]
  161. };
  162. // --- UTILIDADES DE UI ---
  163. const ui = {
  164. showError: (msg) => {
  165. dom.messages.error.textContent = msg;
  166. dom.messages.error.classList.remove('hidden');
  167. dom.messages.success.classList.add('hidden');
  168. },
  169. showSuccess: (msg) => {
  170. dom.messages.success.textContent = msg;
  171. dom.messages.success.classList.remove('hidden');
  172. dom.messages.error.classList.add('hidden');
  173. },
  174. hideMessages: () => {
  175. dom.messages.error.classList.add('hidden');
  176. dom.messages.success.classList.add('hidden');
  177. },
  178. setLoading: (btn, isLoading, text) => {
  179. btn.disabled = isLoading;
  180. btn.textContent = text;
  181. },
  182. updateStepIndicators: () => {
  183. dom.indicators.forEach((el, index) => {
  184. const circle = el.querySelector('.step-circle');
  185. const title = el.querySelector('.step-title');
  186. const desc = el.querySelector('.step-desc');
  187. const stepNum = index + 1;
  188. // Reset clases base
  189. circle.className = 'step-circle flex-shrink-0 w-6 h-6 rounded-full flex items-center justify-center text-xs font-medium transition-colors';
  190. if (stepNum < state.step) { // Completado
  191. circle.classList.add('bg-green-500', 'text-white');
  192. circle.innerHTML = '✓';
  193. title.className = 'step-title font-medium text-green-600';
  194. desc.className = 'step-desc mt-1 text-green-600';
  195. } else if (stepNum === state.step) { // Actual
  196. circle.classList.add('bg-[#101419]', 'text-white');
  197. circle.textContent = stepNum;
  198. title.className = 'step-title font-medium text-[#101419]';
  199. desc.className = 'step-desc mt-1 text-[#58728d]';
  200. } else { // Pendiente
  201. circle.classList.add('bg-gray-300', 'text-gray-600');
  202. circle.textContent = stepNum;
  203. title.className = 'step-title font-medium text-gray-400';
  204. desc.className = 'step-desc mt-1 text-gray-400';
  205. }
  206. });
  207. },
  208. setStep: (step) => {
  209. state.step = step;
  210. // Ocultar todos
  211. Object.values(dom.forms).forEach(f => f.classList.add('hidden'));
  212. // Gestionar temporizador
  213. if (step !== 2) actions.stopResendTimer();
  214. if (step === 2) actions.startResendTimer();
  215. // Mostrar actual
  216. if (step === 1) { dom.forms.email.classList.remove('hidden'); dom.inputs.email.focus(); }
  217. if (step === 2) { dom.forms.code.classList.remove('hidden'); dom.inputs.code.focus(); }
  218. if (step === 3) { dom.forms.pin.classList.remove('hidden'); dom.inputs.newPin.focus(); }
  219. ui.updateStepIndicators();
  220. ui.hideMessages();
  221. }
  222. };
  223. // --- LOGICA DE NEGOCIO (API) ---
  224. const api = {
  225. sendCode: async (email) => {
  226. const res = await fetch('/recovery', {
  227. method: 'POST',
  228. headers: { 'Content-Type': 'application/json' },
  229. body: JSON.stringify({ email })
  230. });
  231. if (!res.ok) throw new Error((await res.json()).message || 'Error al enviar código');
  232. return res.json();
  233. },
  234. validateCode: async (email, code) => {
  235. const res = await fetch('/recovery/validate', {
  236. method: 'POST',
  237. headers: { 'Content-Type': 'application/json' },
  238. body: JSON.stringify({ email, code })
  239. });
  240. const data = await res.json();
  241. if (res.status === 404) throw new Error('Usuario no encontrado');
  242. if (res.status === 400) throw new Error('Código incorrecto');
  243. if (!res.ok) throw new Error(data.message || 'Error de validación');
  244. return data; // Debe contener { data: { token: '...' } }
  245. },
  246. setPin: async (token, new_pin) => {
  247. const res = await fetch('/api/users/pin-recovery', {
  248. method: 'POST',
  249. headers: {
  250. 'Content-Type': 'application/json',
  251. 'Authorization': 'Bearer ' + token
  252. },
  253. body: JSON.stringify({ email: state.email, token, new_pin })
  254. });
  255. if (!res.ok) throw new Error('Error al establecer el PIN');
  256. return res.json();
  257. }
  258. };
  259. // --- ACCIONES ---
  260. const actions = {
  261. requestEmailCode: async () => {
  262. const email = dom.inputs.email.value.trim();
  263. if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
  264. return ui.showError('Ingresa un correo válido');
  265. }
  266. ui.setLoading(dom.buttons.email, true, 'Enviando...');
  267. try {
  268. await api.sendCode(email);
  269. state.email = email;
  270. dom.messages.emailDisplay.textContent = email;
  271. ui.showSuccess(`Código enviado a ${email}`);
  272. setTimeout(() => ui.setStep(2), 500);
  273. } catch (err) {
  274. ui.showError(err.message);
  275. } finally {
  276. ui.setLoading(dom.buttons.email, false, 'Enviar código');
  277. }
  278. },
  279. submitVerificationCode: async () => {
  280. const code = dom.inputs.code.value.trim();
  281. if (!code || code.length !== 6) return ui.showError('Ingresa el código de 6 dígitos');
  282. ui.setLoading(dom.buttons.code, true, 'Verificando...');
  283. try {
  284. const response = await api.validateCode(state.email, code);
  285. // Ajuste robusto: a veces la API devuelve data.token o data.data.token
  286. state.token = response.token || (response.data && response.data.token);
  287. if (!state.token) throw new Error('Token de seguridad no recibido');
  288. ui.showSuccess('Código verificado');
  289. setTimeout(() => ui.setStep(3), 500);
  290. } catch (err) {
  291. ui.showError(err.message);
  292. dom.inputs.code.select();
  293. } finally {
  294. ui.setLoading(dom.buttons.code, false, 'Verificar código');
  295. }
  296. },
  297. submitNewPin: async () => {
  298. const pin = dom.inputs.newPin.value;
  299. const confirm = dom.inputs.confirmPin.value;
  300. if (pin.length !== 4) return ui.showError('El PIN debe tener 4 dígitos');
  301. if (pin !== confirm) return ui.showError('Los PINs no coinciden');
  302. ui.setLoading(dom.buttons.pin, true, 'Guardando...');
  303. try {
  304. await api.setPin(state.token, pin);
  305. ui.showSuccess('¡PIN actualizado! Redirigiendo...');
  306. setTimeout(() => window.location.href = '/', 1500);
  307. } catch (err) {
  308. ui.showError(err.message);
  309. ui.setLoading(dom.buttons.pin, false, 'Establecer PIN');
  310. }
  311. },
  312. startResendTimer: () => {
  313. state.resendCountdown = 60;
  314. dom.buttons.resend.disabled = true;
  315. actions.updateTimerText();
  316. state.resendTimer = setInterval(() => {
  317. state.resendCountdown--;
  318. actions.updateTimerText();
  319. if (state.resendCountdown <= 0) actions.stopResendTimer();
  320. }, 1000);
  321. },
  322. stopResendTimer: () => {
  323. if (state.resendTimer) clearInterval(state.resendTimer);
  324. state.resendTimer = null;
  325. state.resendCountdown = 0;
  326. dom.buttons.resend.disabled = false;
  327. dom.buttons.resendText.textContent = 'Reenviar';
  328. },
  329. updateTimerText: () => {
  330. if (state.resendCountdown > 0) {
  331. dom.buttons.resendText.textContent = `Reenviar (${state.resendCountdown}s)`;
  332. }
  333. }
  334. };
  335. // --- EVENT LISTENERS ---
  336. // Forms Submits
  337. dom.forms.email.addEventListener('submit', (e) => { e.preventDefault(); actions.requestEmailCode(); });
  338. dom.forms.code.addEventListener('submit', (e) => { e.preventDefault(); actions.submitVerificationCode(); });
  339. dom.forms.pin.addEventListener('submit', (e) => { e.preventDefault(); actions.submitNewPin(); });
  340. // Buttons
  341. dom.buttons.back.addEventListener('click', () => ui.setStep(1));
  342. dom.buttons.resend.addEventListener('click', () => {
  343. if (!dom.buttons.resend.disabled) {
  344. actions.stopResendTimer(); // Resetear timer actual si existe
  345. actions.requestEmailCode(); // Reutilizar lógica de envío
  346. // requestEmailCode maneja el timer al pasar al step 2,
  347. // pero como ya estamos en step 2, forzamos el inicio del timer:
  348. actions.startResendTimer();
  349. }
  350. });
  351. // Inputs Formatting
  352. [dom.inputs.code, dom.inputs.newPin, dom.inputs.confirmPin].forEach(input => {
  353. input.addEventListener('input', (e) => {
  354. e.target.value = e.target.value.replace(/\D/g, ''); // Solo números
  355. ui.hideMessages();
  356. });
  357. });
  358. dom.inputs.email.addEventListener('input', ui.hideMessages);
  359. // Inicialización
  360. dom.inputs.email.focus();
  361. </script>
  362. </body>
  363. </html>