Actualizar server.js

This commit is contained in:
2026-02-16 08:10:13 +00:00
parent b83aef7343
commit 211ed8f36c

View File

@@ -3,7 +3,7 @@ import cors from "cors";
import bcrypt from "bcryptjs"; import bcrypt from "bcryptjs";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import pg from "pg"; import pg from "pg";
import crypto from "crypto"; // <--- IMPORTACIÓN CORREGIDA import crypto from "crypto";
const { Pool } = pg; const { Pool } = pg;
const app = express(); const app = express();
@@ -336,8 +336,8 @@ app.get("/public/assignment/:token", async (req, res) => {
FROM assignment_pings ap FROM assignment_pings ap
JOIN scraped_services s ON ap.scraped_id = s.id JOIN scraped_services s ON ap.scraped_id = s.id
JOIN users u ON ap.user_id = u.id JOIN users u ON ap.user_id = u.id
WHERE ap.token = $1 AND ap.status = 'pending' AND ap.expires_at > CAST($2 AS TIMESTAMP) WHERE ap.token = $1 AND ap.status = 'pending' AND ap.expires_at > CURRENT_TIMESTAMP
`, [token, new Date().toISOString()]); `, [token]);
if (q.rowCount === 0) return res.status(404).json({ ok: false, error: "Enlace caducado" }); if (q.rowCount === 0) return res.status(404).json({ ok: false, error: "Enlace caducado" });
res.json({ ok: true, service: q.rows[0].raw_data, worker: q.rows[0].worker_name }); res.json({ ok: true, service: q.rows[0].raw_data, worker: q.rows[0].worker_name });
} catch (e) { res.status(500).json({ ok: false }); } } catch (e) { res.status(500).json({ ok: false }); }
@@ -350,8 +350,8 @@ app.post("/public/assignment/respond", async (req, res) => {
await client.query('BEGIN'); await client.query('BEGIN');
const q = await client.query( const q = await client.query(
"SELECT * FROM assignment_pings WHERE token = $1 AND status = 'pending' AND expires_at > CAST($2 AS TIMESTAMP)", "SELECT * FROM assignment_pings WHERE token = $1 AND status = 'pending' AND expires_at > CURRENT_TIMESTAMP",
[token, new Date().toISOString()] [token]
); );
if (q.rowCount === 0) throw new Error("Acción caducada"); if (q.rowCount === 0) throw new Error("Acción caducada");
@@ -371,7 +371,7 @@ app.post("/public/assignment/respond", async (req, res) => {
`, [ping.user_id, ping.scraped_id]); `, [ping.user_id, ping.scraped_id]);
} else { } else {
await client.query("UPDATE assignment_pings SET status = 'rejected', expires_at = CAST($2 AS TIMESTAMP) WHERE id = $1", [ping.id, new Date().toISOString()]); await client.query("UPDATE assignment_pings SET status = 'rejected', expires_at = CURRENT_TIMESTAMP WHERE id = $1", [ping.id]);
} }
await client.query('COMMIT'); await client.query('COMMIT');
@@ -388,10 +388,10 @@ app.post("/public/assignment/respond", async (req, res) => {
// 🔐 RUTAS AUTH Y PRIVADAS ( CRM ORIGINAL ) // 🔐 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); 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, $4)", [userId, p, codeHash, expiresAt]); 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]);
await sendWhatsAppCode(p, code); 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(); } }); 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 > NOW() 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=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/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.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) => { app.get("/whatsapp/status", authMiddleware, (req, res, next) => requirePlan(req, res, next, 'whatsapp_enabled'), async (req, res) => {
@@ -433,11 +433,12 @@ app.post("/providers/credentials", authMiddleware, async (req, res) => {
app.get("/providers/scraped", authMiddleware, async (req, res) => { app.get("/providers/scraped", authMiddleware, async (req, res) => {
try { try {
// Pedimos a Postgres que calcule los SEGUNDOS que faltan y enviamos la cuenta exacta
const q = await pool.query(` const q = await pool.query(`
SELECT SELECT
s.*, s.*,
ap.token as active_token, ap.token as active_token,
ap.expires_at as token_expires_at, EXTRACT(EPOCH FROM (ap.expires_at - CURRENT_TIMESTAMP)) as seconds_left,
u.full_name as current_worker_name, u.full_name as current_worker_name,
-- Obtenemos objeto con nombre y teléfono de los operarios que fallaron -- 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)) (SELECT json_agg(json_build_object('name', u2.full_name, 'phone', u2.phone))
@@ -451,7 +452,18 @@ app.get("/providers/scraped", authMiddleware, async (req, res) => {
ORDER BY s.created_at DESC ORDER BY s.created_at DESC
`, [req.user.accountId]); `, [req.user.accountId]);
res.json({ ok: true, services: q.rows }); // 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));
} else if (row.seconds_left <= 0) {
row.token_expires_at = new Date(Date.now() - 1000);
}
delete row.seconds_left;
return row;
});
res.json({ ok: true, services });
} catch (e) { } catch (e) {
console.error("Error en GET scraped:", e.message); console.error("Error en GET scraped:", e.message);
res.status(500).json({ ok: false }); res.status(500).json({ ok: false });
@@ -491,24 +503,19 @@ app.post("/providers/automate/:id", authMiddleware, async (req, res) => {
await pool.query("UPDATE scraped_services SET automation_status = 'in_progress' WHERE id = $1", [id]); await pool.query("UPDATE scraped_services SET automation_status = 'in_progress' WHERE id = $1", [id]);
const worker = workersQ.rows[Math.floor(Math.random() * workersQ.rows.length)]; const worker = workersQ.rows[Math.floor(Math.random() * workersQ.rows.length)];
const token = crypto.randomBytes(16).toString('hex'); const token = crypto.randomBytes(16).toString('hex');
// Calculamos la caducidad puramente en Node (+5 min) // LA SOLUCIÓN DEFINITIVA:
const expiresAt = new Date(Date.now() + 5 * 60 * 1000); // 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
// Insertamos el valor exacto de Node convertido a ISO en la DB const pingRes = await pool.query(`
await pool.query(`
INSERT INTO assignment_pings (scraped_id, user_id, token, expires_at) INSERT INTO assignment_pings (scraped_id, user_id, token, expires_at)
VALUES ($1, $2, $3, CAST($4 AS TIMESTAMP)) VALUES ($1, $2, $3, CURRENT_TIMESTAMP + INTERVAL '5 minutes')
`, [id, worker.id, token, expiresAt.toISOString()]); RETURNING to_char((CURRENT_TIMESTAMP + INTERVAL '5 minutes') AT TIME ZONE 'Europe/Madrid', 'HH24:MI') as hora_limite
`, [id, worker.id, token]);
// Formateamos para WhatsApp forzando la hora de España const horaCaducidad = pingRes.rows[0].hora_limite;
const horaCaducidad = expiresAt.toLocaleTimeString('es-ES', {
hour: '2-digit',
minute: '2-digit',
timeZone: 'Europe/Madrid'
});
// 3. Construir mensaje de WhatsApp con toda la información solicitada // 3. Construir mensaje de WhatsApp con toda la información solicitada
const link = `https://web.integrarepara.es/aceptar.html?t=${token}`; const link = `https://web.integrarepara.es/aceptar.html?t=${token}`;
@@ -779,9 +786,9 @@ setInterval(async () => {
SELECT ap.id, ap.scraped_id, ap.user_id, s.owner_id, s.raw_data SELECT ap.id, ap.scraped_id, ap.user_id, s.owner_id, s.raw_data
FROM assignment_pings ap FROM assignment_pings ap
JOIN scraped_services s ON ap.scraped_id = s.id JOIN scraped_services s ON ap.scraped_id = s.id
WHERE ap.status = 'pending' AND ap.expires_at < CAST($1 AS TIMESTAMP) WHERE ap.status = 'pending' AND ap.expires_at < CURRENT_TIMESTAMP
AND s.automation_status = 'in_progress' AND s.automation_status = 'in_progress'
`, [new Date().toISOString()]); `);
for (const ping of expiredPings.rows) { for (const ping of expiredPings.rows) {
await pool.query("UPDATE assignment_pings SET status = 'expired' WHERE id = $1", [ping.id]); await pool.query("UPDATE assignment_pings SET status = 'expired' WHERE id = $1", [ping.id]);
@@ -797,8 +804,7 @@ setInterval(async () => {
if (nextWorkerQ.rowCount > 0) { if (nextWorkerQ.rowCount > 0) {
const nextW = nextWorkerQ.rows[0]; const nextW = nextWorkerQ.rows[0];
const newToken = crypto.randomBytes(16).toString('hex'); const newToken = crypto.randomBytes(16).toString('hex');
const expiresAt = new Date(Date.now() + 5 * 60 * 1000); await pool.query(`INSERT INTO assignment_pings (scraped_id, user_id, token, expires_at) VALUES ($1, $2, $3, CURRENT_TIMESTAMP + INTERVAL '5 minutes')`, [ping.scraped_id, nextW.id, newToken]);
await pool.query(`INSERT INTO assignment_pings (scraped_id, user_id, token, expires_at) VALUES ($1, $2, $3, CAST($4 AS TIMESTAMP))`, [ping.scraped_id, nextW.id, newToken, expiresAt.toISOString()]);
await sendWhatsAppAuto(nextW.phone, `🛠️ *SERVICIO DISPONIBLE*\nEl anterior compañero no respondió. Es tu turno:\n🔗 https://integrarepara.es/aceptar.html?t=${newToken}`); await sendWhatsAppAuto(nextW.phone, `🛠️ *SERVICIO DISPONIBLE*\nEl anterior compañero no respondió. Es tu turno:\n🔗 https://integrarepara.es/aceptar.html?t=${newToken}`);
} else { } else {
await pool.query("UPDATE scraped_services SET automation_status = 'failed' WHERE id = $1", [ping.scraped_id]); await pool.query("UPDATE scraped_services SET automation_status = 'failed' WHERE id = $1", [ping.scraped_id]);