| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403 |
- <!DOCTYPE html>
- <html lang="es">
- <head>
- <meta charset="UTF-8" />
- <title>Recuperar PIN - Biergarten Klein</title>
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
-
- <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
- <meta http-equiv="Pragma" content="no-cache">
- <meta http-equiv="Expires" content="0">
-
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
- <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">
- <script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
- <script>
- tailwind.config = {
- theme: {
- extend: {
- colors: {
- 'custom-dark': '#101419',
- 'custom-dark-hover': '#37404a',
- 'gray-50': '#f9fafb',
- 'gray-100': '#f3f4f6',
- }
- }
- }
- }
- </script>
- </head>
- <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;'>
-
- <div class="w-full max-w-md">
- <div class="text-center mb-8">
- <div class="inline-flex items-center justify-center w-16 h-16 bg-[#101419] rounded-full mb-4">
- <svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
- <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"/>
- </svg>
- </div>
- <h1 class="text-[26px] font-bold text-[#101419] tracking-tight mb-2">¿Olvidaste tu PIN?</h1>
- <p class="text-[#58728d] text-sm leading-relaxed">No te preocupes, te ayudamos a recuperar el acceso a tu cuenta</p>
- </div>
- <div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 mb-6">
- <div class="space-y-4">
- <div class="flex items-start space-x-3" id="step1Indicator">
- <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>
- <div class="text-sm">
- <p class="step-title font-medium text-[#101419]">Ingresa tu correo</p>
- <p class="step-desc text-[#58728d] mt-1">Te enviaremos un código</p>
- </div>
- </div>
- <div class="flex items-start space-x-3" id="step2Indicator">
- <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>
- <div class="text-sm">
- <p class="step-title font-medium text-gray-400">Código de verificación</p>
- <p class="step-desc text-gray-400 mt-1">Ingresa el código de 6 dígitos</p>
- </div>
- </div>
- <div class="flex items-start space-x-3" id="step3Indicator">
- <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>
- <div class="text-sm">
- <p class="step-title font-medium text-gray-400">Nuevo PIN</p>
- <p class="step-desc text-gray-400 mt-1">Crea tu nuevo PIN</p>
- </div>
- </div>
- </div>
- </div>
- <form id="emailForm" class="bg-white rounded-xl shadow-sm border border-gray-200 p-8 space-y-6">
- <div class="text-center">
- <h2 class="text-[19px] font-bold text-[#101419] mb-2">Ingresa tu correo</h2>
- <p class="text-sm text-[#58728d]">Te enviaremos un código de verificación</p>
- </div>
- <div>
- <label for="emailInput" class="block text-sm font-medium text-[#101419] mb-2">Correo electrónico</label>
- <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 />
- </div>
- <div class="space-y-3">
- <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>
- <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>
- </div>
- </form>
- <form id="codeForm" class="bg-white rounded-xl shadow-sm border border-gray-200 p-8 space-y-6 hidden">
- <div class="text-center">
- <h2 class="text-[19px] font-bold text-[#101419] mb-2">Código de verificación</h2>
- <p class="text-sm text-[#58728d]">Ingresa el código enviado a <span id="emailDisplay" class="font-medium"></span></p>
- </div>
- <div>
- <label for="codeInput" class="block text-sm font-medium text-[#101419] mb-2">Código (6 dígitos)</label>
- <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 />
- </div>
- <div class="text-center">
- <button type="button" id="resendCodeBtn" class="text-sm text-[#58728d] hover:text-[#101419] disabled:opacity-50 transition-colors">
- ¿No recibiste el código? <span class="font-medium" id="resendText">Reenviar</span>
- </button>
- </div>
- <div class="space-y-3">
- <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>
- <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>
- </div>
- </form>
- <form id="pinForm" class="bg-white rounded-xl shadow-sm border border-gray-200 p-8 space-y-6 hidden">
- <div class="text-center">
- <h2 class="text-[19px] font-bold text-[#101419] mb-2">Crea tu nuevo PIN</h2>
- <p class="text-sm text-[#58728d]">Ingresa un PIN de 4 dígitos</p>
- </div>
- <div class="space-y-4">
- <div>
- <label for="newPinInput" class="block text-sm font-medium text-[#101419] mb-2">Nuevo PIN</label>
- <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 />
- </div>
- <div>
- <label for="confirmPinInput" class="block text-sm font-medium text-[#101419] mb-2">Confirmar PIN</label>
- <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 />
- </div>
- <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">
- <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>
- <p>Elige un PIN seguro pero fácil de recordar.</p>
- </div>
- </div>
- <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>
- </form>
- <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>
- <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>
- </div>
- <script>
- // --- ESTADO GLOBAL ---
- const state = {
- step: 1,
- email: '',
- token: '',
- resendTimer: null,
- resendCountdown: 0
- };
- // --- REFERENCIAS DOM ---
- const dom = {
- forms: {
- email: document.getElementById('emailForm'),
- code: document.getElementById('codeForm'),
- pin: document.getElementById('pinForm')
- },
- inputs: {
- email: document.getElementById('emailInput'),
- code: document.getElementById('codeInput'),
- newPin: document.getElementById('newPinInput'),
- confirmPin: document.getElementById('confirmPinInput')
- },
- buttons: {
- email: document.getElementById('emailSubmitBtn'),
- code: document.getElementById('codeSubmitBtn'),
- pin: document.getElementById('pinSubmitBtn'),
- resend: document.getElementById('resendCodeBtn'),
- resendText: document.getElementById('resendText'),
- back: document.getElementById('backToEmailBtn')
- },
- messages: {
- error: document.getElementById('errorMessage'),
- success: document.getElementById('successMessage'),
- emailDisplay: document.getElementById('emailDisplay')
- },
- indicators: [
- document.getElementById('step1Indicator'),
- document.getElementById('step2Indicator'),
- document.getElementById('step3Indicator')
- ]
- };
- // --- UTILIDADES DE UI ---
- const ui = {
- showError: (msg) => {
- dom.messages.error.textContent = msg;
- dom.messages.error.classList.remove('hidden');
- dom.messages.success.classList.add('hidden');
- },
- showSuccess: (msg) => {
- dom.messages.success.textContent = msg;
- dom.messages.success.classList.remove('hidden');
- dom.messages.error.classList.add('hidden');
- },
- hideMessages: () => {
- dom.messages.error.classList.add('hidden');
- dom.messages.success.classList.add('hidden');
- },
- setLoading: (btn, isLoading, text) => {
- btn.disabled = isLoading;
- btn.textContent = text;
- },
- updateStepIndicators: () => {
- dom.indicators.forEach((el, index) => {
- const circle = el.querySelector('.step-circle');
- const title = el.querySelector('.step-title');
- const desc = el.querySelector('.step-desc');
- const stepNum = index + 1;
- // Reset clases base
- circle.className = 'step-circle flex-shrink-0 w-6 h-6 rounded-full flex items-center justify-center text-xs font-medium transition-colors';
-
- if (stepNum < state.step) { // Completado
- circle.classList.add('bg-green-500', 'text-white');
- circle.innerHTML = '✓';
- title.className = 'step-title font-medium text-green-600';
- desc.className = 'step-desc mt-1 text-green-600';
- } else if (stepNum === state.step) { // Actual
- circle.classList.add('bg-[#101419]', 'text-white');
- circle.textContent = stepNum;
- title.className = 'step-title font-medium text-[#101419]';
- desc.className = 'step-desc mt-1 text-[#58728d]';
- } else { // Pendiente
- circle.classList.add('bg-gray-300', 'text-gray-600');
- circle.textContent = stepNum;
- title.className = 'step-title font-medium text-gray-400';
- desc.className = 'step-desc mt-1 text-gray-400';
- }
- });
- },
- setStep: (step) => {
- state.step = step;
- // Ocultar todos
- Object.values(dom.forms).forEach(f => f.classList.add('hidden'));
-
- // Gestionar temporizador
- if (step !== 2) actions.stopResendTimer();
- if (step === 2) actions.startResendTimer();
- // Mostrar actual
- if (step === 1) { dom.forms.email.classList.remove('hidden'); dom.inputs.email.focus(); }
- if (step === 2) { dom.forms.code.classList.remove('hidden'); dom.inputs.code.focus(); }
- if (step === 3) { dom.forms.pin.classList.remove('hidden'); dom.inputs.newPin.focus(); }
- ui.updateStepIndicators();
- ui.hideMessages();
- }
- };
- // --- LOGICA DE NEGOCIO (API) ---
- const api = {
- sendCode: async (email) => {
- const res = await fetch('/recovery', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ email })
- });
- if (!res.ok) throw new Error((await res.json()).message || 'Error al enviar código');
- return res.json();
- },
- validateCode: async (email, code) => {
- const res = await fetch('/recovery/validate', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ email, code })
- });
- const data = await res.json();
- if (res.status === 404) throw new Error('Usuario no encontrado');
- if (res.status === 400) throw new Error('Código incorrecto');
- if (!res.ok) throw new Error(data.message || 'Error de validación');
- return data; // Debe contener { data: { token: '...' } }
- },
- setPin: async (token, new_pin) => {
- const res = await fetch('/api/users/pin-recovery', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'Authorization': 'Bearer ' + token
- },
- body: JSON.stringify({ email: state.email, token, new_pin })
- });
- if (!res.ok) throw new Error('Error al establecer el PIN');
- return res.json();
- }
- };
- // --- ACCIONES ---
- const actions = {
- requestEmailCode: async () => {
- const email = dom.inputs.email.value.trim();
- if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
- return ui.showError('Ingresa un correo válido');
- }
- ui.setLoading(dom.buttons.email, true, 'Enviando...');
-
- try {
- await api.sendCode(email);
- state.email = email;
- dom.messages.emailDisplay.textContent = email;
- ui.showSuccess(`Código enviado a ${email}`);
- setTimeout(() => ui.setStep(2), 500);
- } catch (err) {
- ui.showError(err.message);
- } finally {
- ui.setLoading(dom.buttons.email, false, 'Enviar código');
- }
- },
- submitVerificationCode: async () => {
- const code = dom.inputs.code.value.trim();
- if (!code || code.length !== 6) return ui.showError('Ingresa el código de 6 dígitos');
- ui.setLoading(dom.buttons.code, true, 'Verificando...');
- try {
- const response = await api.validateCode(state.email, code);
- // Ajuste robusto: a veces la API devuelve data.token o data.data.token
- state.token = response.token || (response.data && response.data.token);
-
- if (!state.token) throw new Error('Token de seguridad no recibido');
- ui.showSuccess('Código verificado');
- setTimeout(() => ui.setStep(3), 500);
- } catch (err) {
- ui.showError(err.message);
- dom.inputs.code.select();
- } finally {
- ui.setLoading(dom.buttons.code, false, 'Verificar código');
- }
- },
- submitNewPin: async () => {
- const pin = dom.inputs.newPin.value;
- const confirm = dom.inputs.confirmPin.value;
- if (pin.length !== 4) return ui.showError('El PIN debe tener 4 dígitos');
- if (pin !== confirm) return ui.showError('Los PINs no coinciden');
- ui.setLoading(dom.buttons.pin, true, 'Guardando...');
- try {
- await api.setPin(state.token, pin);
- ui.showSuccess('¡PIN actualizado! Redirigiendo...');
- setTimeout(() => window.location.href = '/', 1500);
- } catch (err) {
- ui.showError(err.message);
- ui.setLoading(dom.buttons.pin, false, 'Establecer PIN');
- }
- },
- startResendTimer: () => {
- state.resendCountdown = 60;
- dom.buttons.resend.disabled = true;
- actions.updateTimerText();
-
- state.resendTimer = setInterval(() => {
- state.resendCountdown--;
- actions.updateTimerText();
- if (state.resendCountdown <= 0) actions.stopResendTimer();
- }, 1000);
- },
- stopResendTimer: () => {
- if (state.resendTimer) clearInterval(state.resendTimer);
- state.resendTimer = null;
- state.resendCountdown = 0;
- dom.buttons.resend.disabled = false;
- dom.buttons.resendText.textContent = 'Reenviar';
- },
- updateTimerText: () => {
- if (state.resendCountdown > 0) {
- dom.buttons.resendText.textContent = `Reenviar (${state.resendCountdown}s)`;
- }
- }
- };
- // --- EVENT LISTENERS ---
-
- // Forms Submits
- dom.forms.email.addEventListener('submit', (e) => { e.preventDefault(); actions.requestEmailCode(); });
- dom.forms.code.addEventListener('submit', (e) => { e.preventDefault(); actions.submitVerificationCode(); });
- dom.forms.pin.addEventListener('submit', (e) => { e.preventDefault(); actions.submitNewPin(); });
- // Buttons
- dom.buttons.back.addEventListener('click', () => ui.setStep(1));
- dom.buttons.resend.addEventListener('click', () => {
- if (!dom.buttons.resend.disabled) {
- actions.stopResendTimer(); // Resetear timer actual si existe
- actions.requestEmailCode(); // Reutilizar lógica de envío
- // requestEmailCode maneja el timer al pasar al step 2,
- // pero como ya estamos en step 2, forzamos el inicio del timer:
- actions.startResendTimer();
- }
- });
- // Inputs Formatting
- [dom.inputs.code, dom.inputs.newPin, dom.inputs.confirmPin].forEach(input => {
- input.addEventListener('input', (e) => {
- e.target.value = e.target.value.replace(/\D/g, ''); // Solo números
- ui.hideMessages();
- });
- });
- dom.inputs.email.addEventListener('input', ui.hideMessages);
- // Inicialización
- dom.inputs.email.focus();
- </script>
- </body>
- </html>
|