diff --git a/server.js b/server.js index bdb0a0e..65f6acd 100644 --- a/server.js +++ b/server.js @@ -906,6 +906,79 @@ async function procesarConIA(ownerId, mensajeCliente, datosExpediente) { } } +// ========================================== +// 馃摫 OTP PARA PORTAL DEL CLIENTE (ACCESO WEB) +// ========================================== +app.post("/public/auth/request-otp", async (req, res) => { + try { + const { phone, owner_id } = req.body; + if (!phone || !owner_id) return res.status(400).json({ ok: false, error: "Faltan datos" }); + + const p = normalizePhone(phone); + // Generamos un c贸digo aleatorio de 4 d铆gitos + const code = String(Math.floor(1000 + Math.random() * 9000)); + const codeHash = await bcrypt.hash(code, 10); + + // Lo guardamos en la tabla de login_codes (ponemos user_id NULL porque es un cliente, no un operario) + await pool.query(` + INSERT INTO login_codes (user_id, phone, code_hash, purpose, expires_at) + VALUES (NULL, $1, $2, 'client_portal', NOW() + INTERVAL '15 minutes') + `, [p, codeHash]); + + const msg = `馃憢 *Asistencia T茅cnica*\n\nTu c贸digo seguro de acceso al portal es: *${code}*\n\nCaduca en 15 minutos.`; + + // Enviamos el WhatsApp usando la instancia de la empresa correspondiente + sendWhatsAppAuto(p, msg, `cliente_${owner_id}`, false).catch(console.error); + + res.json({ ok: true }); + } catch (e) { + console.error("Error solicitando OTP cliente:", e); + res.status(500).json({ ok: false }); + } +}); + +app.post("/public/auth/verify-otp", async (req, res) => { + try { + const { phone, code, owner_id } = req.body; + const p = normalizePhone(phone); + + // 1. Buscamos si el c贸digo existe, es de cliente, y no est谩 caducado + const q = await pool.query(` + SELECT id, code_hash FROM login_codes + WHERE phone = $1 AND purpose = 'client_portal' AND consumed_at IS NULL AND expires_at > NOW() + ORDER BY created_at DESC LIMIT 1 + `, [p]); + + if (q.rowCount === 0) return res.status(400).json({ ok: false, error: "C贸digo inv谩lido o caducado" }); + + // 2. Comprobamos que el c贸digo de 4 d铆gitos coincide + const valid = await bcrypt.compare(String(code), q.rows[0].code_hash); + if (!valid) return res.status(400).json({ ok: false, error: "C贸digo incorrecto" }); + + // 3. Quemamos el c贸digo para que no se pueda usar 2 veces + await pool.query("UPDATE login_codes SET consumed_at = NOW() WHERE id = $1", [q.rows[0].id]); + + // 4. Magia: Buscamos si este cliente ya nos ha llamado antes para pre-rellenar su formulario + const cleanPhoneForSearch = String(phone).replace(/\D/g, ""); + const clientQ = await pool.query( + "SELECT full_name, addresses FROM clients WHERE phone LIKE $1 AND owner_id = $2 LIMIT 1", + [`%${cleanPhoneForSearch}%`, owner_id] + ); + + let clientData = null; + let exists = false; + if (clientQ.rowCount > 0) { + clientData = clientQ.rows[0]; + exists = true; + } + + res.json({ ok: true, exists, client: clientData }); + } catch (e) { + console.error("Error verificando OTP cliente:", e); + res.status(500).json({ ok: false }); + } +}); + // ========================================== // 馃寪 EMBUDO P脷BLICO DE ENTRADA DE CLIENTES // ==========================================