From f23a3b679aef3b179495733ba69cf00760a06077 Mon Sep 17 00:00:00 2001 From: marsalva Date: Mon, 16 Feb 2026 08:22:00 +0000 Subject: [PATCH] Actualizar server.js --- server.js | 72 ++++++++++++++++++++++--------------------------------- 1 file changed, 28 insertions(+), 44 deletions(-) diff --git a/server.js b/server.js index 986bcd2..91eb495 100644 --- a/server.js +++ b/server.js @@ -332,7 +332,7 @@ app.get("/public/assignment/:token", async (req, res) => { try { const { token } = req.params; - // MODO DEBUG: Traemos el registro exista o no, y le pedimos a la BD su hora exacta (db_now) + // Comprobación MODO BLINDADO (Extrae todo, exista o no) const q = await pool.query(` SELECT ap.*, s.raw_data, u.full_name as worker_name, CURRENT_TIMESTAMP as db_now FROM assignment_pings ap @@ -341,37 +341,22 @@ app.get("/public/assignment/:token", async (req, res) => { WHERE ap.token = $1 `, [token]); - if (q.rowCount === 0) { - return res.status(404).json({ ok: false, error: "El enlace no existe en la base de datos." }); - } + if (q.rowCount === 0) return res.status(404).json({ ok: false, error: "Enlace caducado o inexistente" }); const data = q.rows[0]; - - // Hacemos la comprobación manualmente para decidir si mandamos error o éxito const isExpired = data.status !== 'pending' || new Date(data.expires_at) <= new Date(data.db_now); if (isExpired) { - return res.status(404).json({ - ok: false, - error: "Este enlace ha caducado o el servicio ya ha sido asignado.", - debug: { - estado_en_bd: data.status, - hora_limite_bd: data.expires_at, - hora_actual_bd: data.db_now - } - }); + return res.status(404).json({ ok: false, error: "Este enlace ha caducado o ha sido reasignado." }); } res.json({ ok: true, service: data.raw_data, worker: data.worker_name, - debug: { - hora_limite_bd: data.expires_at, - hora_actual_bd: data.db_now - } + debug: { hora_limite_bd: data.expires_at, hora_actual_bd: data.db_now } }); - } catch (e) { res.status(500).json({ ok: false, error: e.message }); } + } catch (e) { res.status(500).json({ ok: false }); } }); app.post("/public/assignment/respond", async (req, res) => { @@ -380,24 +365,29 @@ app.post("/public/assignment/respond", async (req, res) => { const { token, action } = req.body; await client.query('BEGIN'); + // LEEMOS EXACTAMENTE IGUAL QUE EN LA RUTA GET, BLOQUEANDO ERRORES FANTASMA const q = await client.query( - "SELECT * FROM assignment_pings WHERE token = $1 AND status = 'pending' AND expires_at > CURRENT_TIMESTAMP", + "SELECT *, CURRENT_TIMESTAMP as db_now FROM assignment_pings WHERE token = $1 FOR UPDATE", [token] ); - if (q.rowCount === 0) throw new Error("Acción caducada"); + + if (q.rowCount === 0) throw new Error("Enlace no válido o inexistente"); const ping = q.rows[0]; + const isExpired = ping.status !== 'pending' || new Date(ping.expires_at) <= new Date(ping.db_now); + + if (isExpired) throw new Error("El tiempo se agotó justo antes de aceptar."); if (action === 'accept') { await client.query("UPDATE assignment_pings SET status = 'accepted' WHERE id = $1", [ping.id]); - // AÑADIDO: Guardar ID de operario en columna física y JSON + // AÑADIDO: Guardar ID de operario (Forzamos tipo INT para evitar errores de JSONB) await client.query(` UPDATE scraped_services SET status = 'imported', automation_status = 'completed', assigned_to = $1, - raw_data = raw_data || jsonb_build_object('assigned_to', $1) + raw_data = raw_data || jsonb_build_object('assigned_to', $1::int) WHERE id = $2 `, [ping.user_id, ping.scraped_id]); @@ -409,7 +399,8 @@ app.post("/public/assignment/respond", async (req, res) => { res.json({ ok: true }); } catch (e) { await client.query('ROLLBACK'); - res.status(400).json({ ok: false }); + console.error("ERROR AL RESPONDER TURNO:", e.message); // Por si acaso hay otro error + res.status(400).json({ ok: false, error: e.message }); } finally { client.release(); } @@ -419,10 +410,10 @@ app.post("/public/assignment/respond", async (req, res) => { // 🔐 RUTAS AUTH Y PRIVADAS ( CRM ORIGINAL ) // ========================================== -app.post("/auth/register", async (req, res) => { const client = await pool.connect(); try { const { fullName, phone, address, dni, email, password } = req.body; const p = normalizePhone(phone); if (!fullName || !p || !email || !password) return res.status(400).json({ ok: false }); const passwordHash = await bcrypt.hash(password, 10); await client.query('BEGIN'); const insert = await client.query("INSERT INTO users (full_name, phone, address, dni, email, password_hash, role, owner_id, plan_tier) VALUES ($1, $2, $3, $4, $5, $6, 'admin', NULL, 'free') RETURNING id", [fullName, p, address, dni, email, passwordHash]); const userId = insert.rows[0].id; const code = genCode6(); const codeHash = await bcrypt.hash(code, 10); await client.query("INSERT INTO login_codes (user_id, phone, code_hash, expires_at) VALUES ($1, $2, $3, CURRENT_TIMESTAMP + INTERVAL '10 minutes')", [userId, p, codeHash]); +app.post("/auth/register", async (req, res) => { const client = await pool.connect(); try { const { fullName, phone, address, dni, email, password } = req.body; const p = normalizePhone(phone); if (!fullName || !p || !email || !password) return res.status(400).json({ ok: false }); const passwordHash = await bcrypt.hash(password, 10); await client.query('BEGIN'); const insert = await client.query("INSERT INTO users (full_name, phone, address, dni, email, password_hash, role, owner_id, plan_tier) VALUES ($1, $2, $3, $4, $5, $6, 'admin', NULL, 'free') RETURNING id", [fullName, p, address, dni, email, passwordHash]); const userId = insert.rows[0].id; const code = genCode6(); const codeHash = await bcrypt.hash(code, 10); const expiresAt = new Date(Date.now() + 10 * 60 * 1000); await client.query("INSERT INTO login_codes (user_id, phone, code_hash, expires_at) VALUES ($1, $2, $3, CURRENT_TIMESTAMP + INTERVAL '10 minutes')", [userId, p, codeHash]); await sendWhatsAppCode(p, code); await client.query('COMMIT'); res.json({ ok: true, phone: p }); } catch (e) { await client.query('ROLLBACK'); res.status(500).json({ ok: false }); } finally { client.release(); } }); -app.post("/auth/verify", async (req, res) => { try { const { phone, code } = req.body; const p = normalizePhone(phone); const q = await pool.query(`SELECT lc.*, u.id as uid, u.email, u.role, u.owner_id FROM login_codes lc JOIN users u ON lc.user_id = u.id WHERE lc.phone=$1 AND lc.consumed_at IS NULL AND lc.expires_at > CURRENT_TIMESTAMP ORDER BY lc.created_at DESC LIMIT 1`, [p]); if (q.rowCount === 0) return res.status(400).json({ ok: false }); const row = q.rows[0]; if (!(await bcrypt.compare(String(code), row.code_hash))) return res.status(400).json({ ok: false }); await pool.query("UPDATE login_codes SET consumed_at=NOW() WHERE id=$1", [row.id]); await pool.query("UPDATE users SET is_verified=TRUE WHERE id=$1", [row.uid]); res.json({ ok: true, token: signToken({ id: row.uid, email: row.email, phone: p, role: row.role, owner_id: row.owner_id }) }); } catch (e) { res.status(500).json({ ok: false }); } }); +app.post("/auth/verify", async (req, res) => { try { const { phone, code } = req.body; const p = normalizePhone(phone); const q = await pool.query(`SELECT lc.*, u.id as uid, u.email, u.role, u.owner_id FROM login_codes lc JOIN users u ON lc.user_id = u.id WHERE lc.phone=$1 AND lc.consumed_at IS NULL AND lc.expires_at > CURRENT_TIMESTAMP ORDER BY lc.created_at DESC LIMIT 1`, [p]); if (q.rowCount === 0) return res.status(400).json({ ok: false }); const row = q.rows[0]; if (!(await bcrypt.compare(String(code), row.code_hash))) return res.status(400).json({ ok: false }); await pool.query("UPDATE login_codes SET consumed_at=CURRENT_TIMESTAMP WHERE id=$1", [row.id]); await pool.query("UPDATE users SET is_verified=TRUE WHERE id=$1", [row.uid]); res.json({ ok: true, token: signToken({ id: row.uid, email: row.email, phone: p, role: row.role, owner_id: row.owner_id }) }); } catch (e) { res.status(500).json({ ok: false }); } }); app.post("/auth/login", async (req, res) => { try { const { email, password } = req.body; const q = await pool.query("SELECT * FROM users WHERE email=$1", [email]); if (q.rowCount === 0) return res.status(401).json({ ok: false }); let user = null; for (const u of q.rows) { if (await bcrypt.compare(password, u.password_hash)) { user = u; break; } } if (!user) return res.status(401).json({ ok: false }); res.json({ ok: true, token: signToken(user) }); } catch(e) { res.status(500).json({ ok: false }); } }); app.get("/whatsapp/status", authMiddleware, (req, res, next) => requirePlan(req, res, next, 'whatsapp_enabled'), async (req, res) => { @@ -471,7 +462,6 @@ app.get("/providers/scraped", authMiddleware, async (req, res) => { ap.token as active_token, EXTRACT(EPOCH FROM (ap.expires_at - CURRENT_TIMESTAMP)) as seconds_left, u.full_name as current_worker_name, - -- Obtenemos objeto con nombre y teléfono de los operarios que fallaron (SELECT json_agg(json_build_object('name', u2.full_name, 'phone', u2.phone)) FROM assignment_pings ap2 JOIN users u2 ON ap2.user_id = u2.id @@ -483,7 +473,6 @@ app.get("/providers/scraped", authMiddleware, async (req, res) => { ORDER BY s.created_at DESC `, [req.user.accountId]); - // Transformamos esos segundos en una fecha universal perfecta para tu web (automatizacion.html) const services = q.rows.map(row => { if (row.seconds_left && row.seconds_left > 0) { row.token_expires_at = new Date(Date.now() + (row.seconds_left * 1000)); @@ -496,7 +485,6 @@ app.get("/providers/scraped", authMiddleware, async (req, res) => { res.json({ ok: true, services }); } catch (e) { - console.error("Error en GET scraped:", e.message); res.status(500).json({ ok: false }); } }); @@ -508,7 +496,6 @@ app.post("/providers/automate/:id", authMiddleware, async (req, res) => { if (!guild_id || !cp) return res.status(400).json({ ok: false, error: "Faltan datos (Gremio o CP)" }); - // 1. Obtener datos del expediente para el mensaje const serviceQ = await pool.query("SELECT raw_data, provider FROM scraped_services WHERE id = $1", [id]); if (serviceQ.rowCount === 0) return res.status(404).json({ ok: false, error: "Expediente no encontrado" }); @@ -517,11 +504,9 @@ app.post("/providers/automate/:id", authMiddleware, async (req, res) => { const poblacion = raw["Población"] || raw["POBLACION-PROVINCIA"] || "---"; const gremioNombre = raw["Gremio"] || "Servicio General"; - // Limpiar dirección: Quitar números y pisos (regex para detectar números y lo que sigue) const direccionCompleta = raw["Dirección"] || raw["DOMICILIO"] || ""; const direccionLimpia = direccionCompleta.split(/[0-9]/)[0].trim(); - // 2. Buscar operarios disponibles const workersQ = await pool.query(` SELECT u.id, u.full_name, u.phone FROM users u @@ -537,18 +522,19 @@ app.post("/providers/automate/:id", authMiddleware, async (req, res) => { const worker = workersQ.rows[Math.floor(Math.random() * workersQ.rows.length)]; const token = crypto.randomBytes(16).toString('hex'); - // LA SOLUCIÓN DEFINITIVA: - // 1. Postgres inserta la fecha calculando 5 minutos con su reloj - // 2. Le pedimos a Postgres que nos devuelva el texto de la hora ya formateada para Madrid - const pingRes = await pool.query(` + await pool.query(` INSERT INTO assignment_pings (scraped_id, user_id, token, expires_at) VALUES ($1, $2, $3, CURRENT_TIMESTAMP + INTERVAL '5 minutes') - RETURNING to_char((CURRENT_TIMESTAMP + INTERVAL '5 minutes') AT TIME ZONE 'Europe/Madrid', 'HH24:MI') as hora_limite `, [id, worker.id, token]); - const horaCaducidad = pingRes.rows[0].hora_limite; + // CÁLCULO DE HORA 100% FIABLE: Se lo pedimos a Node forzando a España + // Así siempre saldrá "0:40" en lugar de "23:40" en el texto de WhatsApp + const horaCaducidad = new Date(Date.now() + 5 * 60 * 1000).toLocaleTimeString('es-ES', { + hour: '2-digit', + minute: '2-digit', + timeZone: 'Europe/Madrid' + }); - // 3. Construir mensaje de WhatsApp con toda la información solicitada const link = `https://web.integrarepara.es/aceptar.html?t=${token}`; const mensaje = `🛠️ *NUEVO SERVICIO ASIGNADO A TI* @@ -618,7 +604,6 @@ app.put('/providers/scraped/:id', authMiddleware, async (req, res) => { const { automation_status, name, phone, address, status } = req.body; try { - // ACCIÓN PARA LA PAPELERA: Si enviamos solo automation_status, reseteamos el estado if (automation_status) { await pool.query( `UPDATE scraped_services SET automation_status = $1 WHERE id = $2 AND owner_id = $3`, @@ -627,7 +612,6 @@ app.put('/providers/scraped/:id', authMiddleware, async (req, res) => { return res.json({ ok: true }); } - // ACCIÓN PARA ARCHIVAR: Si el frontend manda status 'archived' if (status === 'archived') { await pool.query( `UPDATE scraped_services SET status = 'archived', automation_status = 'manual' WHERE id = $2 AND owner_id = $3`, @@ -636,7 +620,6 @@ app.put('/providers/scraped/:id', authMiddleware, async (req, res) => { return res.json({ ok: true }); } - // EDICIÓN NORMAL: Mantenemos tu lógica de actualizar datos del cliente const current = await pool.query('SELECT raw_data FROM scraped_services WHERE id = $1 AND owner_id = $2', [id, req.user.accountId]); if (current.rows.length === 0) return res.status(404).json({ error: 'No encontrado' }); @@ -817,7 +800,8 @@ setInterval(async () => { SELECT ap.id, ap.scraped_id, ap.user_id, s.owner_id, s.raw_data FROM assignment_pings ap JOIN scraped_services s ON ap.scraped_id = s.id - WHERE ap.status = 'pending' AND ap.expires_at < CURRENT_TIMESTAMP + WHERE ap.status = 'pending' + AND EXTRACT(EPOCH FROM (ap.expires_at - CURRENT_TIMESTAMP)) <= 0 AND s.automation_status = 'in_progress' `);