From 06d67be7caac5282a73b71bacaa26372dbcd4bc4 Mon Sep 17 00:00:00 2001 From: marsalva Date: Tue, 3 Mar 2026 22:46:25 +0000 Subject: [PATCH] Actualizar server.js --- server.js | 196 +++++++++--------------------------------------------- 1 file changed, 33 insertions(+), 163 deletions(-) diff --git a/server.js b/server.js index 0b20703..ea5ed4d 100644 --- a/server.js +++ b/server.js @@ -381,155 +381,19 @@ async function requirePlan(req, res, next, feature) { // 🔐 RUTAS DE AUTENTICACIÓN (LOGIN) - RESTAURADAS // ========================================== -app.post("/auth/login", async (req, res) => { - try { - const { email, password } = req.body; - if (!email || !password) return res.status(400).json({ ok: false, error: "Faltan credenciales" }); - - // Buscamos al usuario por email o por teléfono - const q = await pool.query("SELECT * FROM users WHERE email = $1 OR phone = $1", [email]); - if (q.rowCount === 0) return res.status(401).json({ ok: false, error: "Usuario no encontrado" }); - - const user = q.rows[0]; - if (user.status !== 'active') return res.status(401).json({ ok: false, error: "Cuenta desactivada. Contacta con el administrador." }); - - const valid = await bcrypt.compare(password, user.password_hash); - if (!valid) return res.status(401).json({ ok: false, error: "Contraseña incorrecta" }); - - const token = signToken(user); - res.json({ ok: true, token, role: user.role, name: user.full_name }); - } catch (e) { - console.error("Login error:", e); - res.status(500).json({ ok: false, error: "Error de servidor al iniciar sesión" }); - } -}); - -app.post("/auth/register", async (req, res) => { - const client = await pool.connect(); - try { - const { fullName, email, phone, password } = req.body; - if (!email || !password) return res.status(400).json({ ok: false, error: "Faltan datos requeridos" }); - - const hash = await bcrypt.hash(password, 10); - await client.query('BEGIN'); - - const insert = await client.query( - "INSERT INTO users (full_name, email, phone, password_hash, role, status) VALUES ($1, $2, $3, $4, 'admin', 'active') RETURNING id", - [fullName || 'Admin', email, normalizePhone(phone), hash] - ); - const newId = insert.rows[0].id; - - // Al ser registro, él es su propio dueño (owner_id = id) - await client.query("UPDATE users SET owner_id = id WHERE id = $1", [newId]); - await client.query('COMMIT'); - res.json({ ok: true, message: "Usuario creado. Ya puedes iniciar sesión." }); - } catch (e) { - await client.query('ROLLBACK'); - console.error("Register error:", e); - res.status(400).json({ ok: false, error: "El correo o teléfono ya están en uso." }); - } finally { - client.release(); - } -}); - -app.get("/auth/me", authMiddleware, async (req, res) => { - try { - const q = await pool.query("SELECT id, full_name, email, role, company_slug, plan_tier FROM users WHERE id = $1", [req.user.sub]); - if(q.rowCount === 0) return res.status(404).json({ok: false}); - res.json({ ok: true, user: q.rows[0] }); - } catch(e) { - res.status(500).json({ ok: false }); - } -}); - -// --- WHATSAPP UTILS --- -async function sendWhatsAppCode(phone, code) { - if (!EVOLUTION_BASE_URL || !EVOLUTION_API_KEY || !EVOLUTION_INSTANCE) { console.error("❌ Faltan datos WhatsApp"); return; } - try { - await fetch(`${EVOLUTION_BASE_URL.replace(/\/$/, "")}/message/sendText/${EVOLUTION_INSTANCE}`, { - method: "POST", headers: { "Content-Type": "application/json", "apikey": EVOLUTION_API_KEY }, - body: JSON.stringify({ number: phone.replace("+", ""), text: `🔐 Código: *${code}*` }) - }); - } catch (e) { console.error("Error envío WA:", e.message); } -} - -async function sendWhatsAppAuto(originalPhone, text, instanceName, useDelay = true) { - if (!EVOLUTION_BASE_URL || !EVOLUTION_API_KEY || !instanceName) return false; - - const TEST_PHONE = "34667248132"; // TU NÚMERO PROTEGIDO - const phone = TEST_PHONE; - - try { - console.log(`\n📲 [MODO PRUEBA] WA a ${originalPhone} redirigido a -> ${phone}`); - - let payloadConEscribiendo; - const typingTimeMs = Math.min(Math.max(text.length * 30, 1500), 8000); - const textWithNotice = `*(PRUEBA - Iba para: ${originalPhone})*\n\n` + text; - - if(useDelay) { - payloadConEscribiendo = { - number: phone.replace("+", ""), - text: textWithNotice, - options: { delay: typingTimeMs, presence: "composing" } - }; - } else { - payloadConEscribiendo = { number: phone.replace("+", ""), text: textWithNotice }; - } - - const res = await fetch(`${EVOLUTION_BASE_URL.replace(/\/$/, "")}/message/sendText/${instanceName}`, { - method: "POST", - headers: { "Content-Type": "application/json", "apikey": EVOLUTION_API_KEY }, - body: JSON.stringify(payloadConEscribiendo) - }); - - if (!res.ok && useDelay) { - const payloadSeguro = { number: phone.replace("+", ""), text: textWithNotice }; - const res2 = await fetch(`${EVOLUTION_BASE_URL.replace(/\/$/, "")}/message/sendText/${instanceName}`, { - method: "POST", headers: { "Content-Type": "application/json", "apikey": EVOLUTION_API_KEY }, - body: JSON.stringify(payloadSeguro) - }); - - if (!res2.ok) return false; // Falló el plan B - return true; // Éxito en plan B - } else if (res.ok) { - return true; // Éxito a la primera - } else { - return false; // Falló y no había delay - } - } catch (e) { - console.error("❌ Error crítico en WA:", e.message); - return false; - } -} - -async function ensureInstance(instanceName) { - if (!EVOLUTION_BASE_URL || !EVOLUTION_API_KEY) throw new Error("Faltan variables EVOLUTION"); - const baseUrl = EVOLUTION_BASE_URL.replace(/\/$/, ""); - const headers = { "Content-Type": "application/json", "apikey": EVOLUTION_API_KEY.trim() }; - const checkRes = await fetch(`${baseUrl}/instance/connectionState/${instanceName}`, { headers }); - if (checkRes.status === 404) { - await fetch(`${baseUrl}/instance/create`, { - method: 'POST', headers, - body: JSON.stringify({ instanceName: instanceName, qrcode: true, integration: "WHATSAPP-BAILEYS" }) - }); - } - return { baseUrl, headers }; -} - - -// ========================================== -// 🔗 PORTAL PÚBLICO DEL CLIENTE -// ========================================== app.get("/public/portal/:token", async (req, res) => { try { const { token } = req.params; + // 1. Buscamos al cliente por su token mágico const qClient = await pool.query("SELECT * FROM clients WHERE portal_token = $1", [token]); if (qClient.rowCount === 0) return res.status(404).json({ ok: false, error: "Enlace no válido" }); const client = qClient.rows[0]; const ownerId = client.owner_id; + const clientId = client.id; + // 2. Buscamos los datos de la empresa para personalizar el portal const qConfig = await pool.query("SELECT full_name, company_logo, portal_settings FROM users WHERE id = $1", [ownerId]); const userData = qConfig.rows[0] || {}; @@ -538,10 +402,13 @@ app.get("/public/portal/:token", async (req, res) => { logo: userData.company_logo || null }; - // AQUÍ ESTÁ LA MAGIA: Hacemos LEFT JOIN con service_statuses para obtener el nombre real + // 3. Obtenemos los servicios. + // AHORA BUSCAMOS POR client_id. Si el client_id es nulo (expedientes viejos), buscamos por teléfono en el JSON. + const cleanPhoneToMatch = String(client.phone || "").replace(/\D/g, "").slice(-9); + const qServices = await pool.query(` SELECT - s.id, s.service_ref, s.is_urgent, s.raw_data, s.created_at, + s.id, s.service_ref, s.is_urgent, s.raw_data, s.created_at, s.client_id, st.name as real_status_name, st.is_final as is_status_final, u.full_name as worker_name, @@ -551,31 +418,34 @@ app.get("/public/portal/:token", async (req, res) => { LEFT JOIN service_statuses st ON st.id::text = (s.raw_data->>'status_operativo')::text WHERE s.owner_id = $1 AND s.provider != 'SYSTEM_BLOCK' + AND ( + s.client_id = $2 + OR + (s.client_id IS NULL AND REPLACE(s.raw_data->>'Teléfono', ' ', '') LIKE $3) + OR + (s.client_id IS NULL AND REPLACE(s.raw_data->>'TELEFONO', ' ', '') LIKE $3) + OR + (s.client_id IS NULL AND REPLACE(s.raw_data->>'TELEFONOS', ' ', '') LIKE $3) + ) ORDER BY s.created_at DESC - `, [ownerId]); + `, [ownerId, clientId, `%${cleanPhoneToMatch}%`]); - const cleanPhoneToMatch = String(client.phone || "").replace(/\D/g, "").slice(-9); - - const formattedServices = qServices.rows - .filter(s => { - const rawString = JSON.stringify(s.raw_data || "").replace(/\D/g, ""); - return rawString.includes(cleanPhoneToMatch); - }) - .map(s => { - return { - id: s.id, - title: s.is_urgent ? `🚨 URGENTE: #${s.service_ref}` : `Expediente #${s.service_ref}`, - description: s.raw_data?.["Descripción"] || s.raw_data?.["DESCRIPCION"] || "Aviso de reparación", - status_name: s.real_status_name || "En gestión", // AHORA SÍ PASAMOS EL NOMBRE REAL - is_final: s.is_status_final || false, - scheduled_date: s.raw_data?.scheduled_date || "", - scheduled_time: s.raw_data?.scheduled_time || "", - assigned_worker: s.worker_name || null, - worker_phone: s.worker_phone || null, - raw_data: s.raw_data - }; - }); + const formattedServices = qServices.rows.map(s => { + return { + id: s.id, + title: s.is_urgent ? `🚨 URGENTE: #${s.service_ref}` : `Expediente #${s.service_ref}`, + description: s.raw_data?.["Descripción"] || s.raw_data?.["DESCRIPCION"] || "Aviso de reparación", + status_name: s.real_status_name || "En gestión", + is_final: s.is_status_final || false, + scheduled_date: s.raw_data?.scheduled_date || "", + scheduled_time: s.raw_data?.scheduled_time || "", + assigned_worker: s.worker_name || null, + worker_phone: s.worker_phone || null, + raw_data: s.raw_data + }; + }); + // Incluso si no hay servicios, devolvemos el portal vacío para que no dé error 404 res.json({ ok: true, client: { name: client.full_name }, company, services: formattedServices }); } catch (e) {