From 1663578f3e25a81b76bcf72d59be644800163a3b Mon Sep 17 00:00:00 2001 From: marsalva Date: Sun, 8 Feb 2026 23:17:20 +0000 Subject: [PATCH] Actualizar server.js --- server.js | 61 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 39 insertions(+), 22 deletions(-) diff --git a/server.js b/server.js index 835c240..91fdf8c 100644 --- a/server.js +++ b/server.js @@ -3,6 +3,7 @@ import cors from "cors"; import bcrypt from "bcryptjs"; import jwt from "jsonwebtoken"; import pg from "pg"; +// NOTA: No importamos node-fetch. Usamos el nativo de Node v18+. const { Pool } = pg; const app = express(); @@ -137,7 +138,7 @@ async function autoUpdateDB() { ); `); - // 3. SERVICIOS Y LOGS + // 3. SERVICIOS await client.query(` CREATE TABLE IF NOT EXISTS services ( id SERIAL PRIMARY KEY, @@ -175,7 +176,10 @@ async function autoUpdateDB() { ); `); - // 4. PARCHE DE REPARACIÓN + // 4. SEEDING (CARGA DE DATOS INE) + await seedSpainData(client); + + // 5. PARCHE DE REPARACIÓN await client.query(` DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='services' AND column_name='client_id') THEN ALTER TABLE services ADD COLUMN client_id INT REFERENCES clients(id) ON DELETE SET NULL; END IF; @@ -203,26 +207,26 @@ async function autoUpdateDB() { END $$; `); - // 5. CARGA MASIVA INE (AHORA SÍ) - await seedSpainData(client); - console.log("✅ DB Sincronizada."); } catch (e) { console.error("❌ Error DB:", e); } finally { client.release(); } } -// 🌍 CARGA MASIVA INE (LÓGICA MEJORADA) +// 🌍 CARGA MASIVA INE (SIN NODE-FETCH y CON CHEQUEO INTELIGENTE) async function seedSpainData(client) { try { - // Comprobamos si hay PUEBLOS (no provincias), porque provincias puede haber alguna suelta + // Comprobamos si hay PUEBLOS (si hay menos de 500, asumimos que está incompleta y recargamos) const count = await client.query("SELECT COUNT(*) FROM towns"); - if (parseInt(count.rows[0].count) > 100) return; // Si hay más de 100 pueblos, asumimos que está cargada + if (parseInt(count.rows[0].count) > 500) return; - console.log("📥 Detectada DB vacía de poblaciones. Descargando datos del INE..."); + console.log("📥 Base de datos de poblaciones incompleta. Iniciando descarga del INE..."); + // Usamos fetch nativo (Node 18+) const response = await fetch("https://raw.githubusercontent.com/frontid/comunidades-provincias-poblaciones/master/poblaciones.json"); + if (!response.ok) throw new Error("Fallo al descargar JSON"); + const listFull = await response.json(); - console.log(`🇪🇸 Insertando ${listFull.length} registros... (Esto puede tardar un poco)`); + console.log(`🇪🇸 Insertando ${listFull.length} poblaciones... Por favor, espera.`); await client.query("BEGIN"); @@ -230,31 +234,33 @@ async function seedSpainData(client) { const provincesSet = new Set(); listFull.forEach(item => provincesSet.add(item.parent_op)); - // Mapa para guardar el ID real de cada provincia const provinceMap = new Map(); for (const provName of provincesSet) { - // Insertamos si no existe, y recuperamos el ID (incluso si ya existía) + // Insertar si no existe await client.query("INSERT INTO provinces (name) VALUES ($1) ON CONFLICT (name) DO NOTHING", [provName]); + // Obtener ID (sea nuevo o viejo) const res = await client.query("SELECT id FROM provinces WHERE name=$1", [provName]); - provinceMap.set(provName, res.rows[0].id); + if (res.rows[0]) provinceMap.set(provName, res.rows[0].id); } - // 2. Insertar Pueblos + // 2. Insertar Pueblos (Solo si tenemos la provincia) for (const item of listFull) { const pid = provinceMap.get(item.parent_op); if (pid) { - // Insertamos pueblo asociado a la provincia encontrada + // Usamos INSERT simple, si hay duplicados no pasa nada grave en esta fase masiva await client.query("INSERT INTO towns (province_id, name) VALUES ($1, $2)", [pid, item.label]); } } await client.query("COMMIT"); - console.log("✅ Datos de España cargados correctamente."); + console.log("✅ Datos de España (8.000+ municipios) cargados correctamente."); } catch (e) { await client.query("ROLLBACK"); - console.error("❌ Error Seeding:", e); + console.error("❌ Error Seeding INE:", e); + // Fallback de emergencia para que la app no rompa + await client.query("INSERT INTO provinces (name) VALUES ('Cádiz') ON CONFLICT DO NOTHING"); } } @@ -272,9 +278,19 @@ app.post("/auth/login", async (req, res) => { try { const { email, password } = app.post("/auth/forgot-password", async (req, res) => { try { const { dni, phone } = req.body; const p = normalizePhone(phone); const q = await pool.query("SELECT id FROM users WHERE dni=$1 AND phone=$2", [dni, p]); if (q.rowCount === 0) return res.status(404).json({ ok: false }); const uid = q.rows[0].id; const code = genCode6(); const hash = await bcrypt.hash(code, 10); await pool.query("INSERT INTO login_codes (user_id, phone, code_hash, purpose, expires_at) VALUES ($1, $2, $3, 'password_reset', $4)", [uid, p, hash, new Date(Date.now()+600000)]); await sendWhatsAppCode(p, code); res.json({ ok: true }); } catch (e) { res.status(500).json({ ok: false }); } }); app.post("/auth/reset-password", async (req, res) => { const client = await pool.connect(); try { const { phone, code, newPassword } = req.body; const p = normalizePhone(phone); const q = await client.query(`SELECT lc.*, u.id as uid FROM login_codes lc JOIN users u ON lc.user_id=u.id WHERE lc.phone=$1 AND lc.purpose='password_reset' 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}); const hash=await bcrypt.hash(newPassword, 10); await client.query('BEGIN'); await client.query("UPDATE users SET password_hash=$1 WHERE id=$2",[hash, row.uid]); await client.query("UPDATE login_codes SET consumed_at=NOW() WHERE id=$1",[row.id]); await client.query('COMMIT'); res.json({ok:true}); } catch(e){await client.query('ROLLBACK'); res.status(500).json({ok:false});} finally{client.release();} }); -// APIS GEOGRÁFICAS -app.get("/provinces", authMiddleware, async (req, res) => { try { const q = await pool.query("SELECT * FROM provinces ORDER BY name ASC"); res.json({ ok: true, provinces: q.rows }); } catch (e) { res.status(500).json({ ok: false }); } }); -app.get("/provinces/:id/towns", authMiddleware, async (req, res) => { try { const q = await pool.query("SELECT * FROM towns WHERE province_id=$1 ORDER BY name ASC", [req.params.id]); res.json({ ok: true, towns: q.rows }); } catch (e) { res.status(500).json({ ok: false }); } }); +// ========================= +// API GEOGRÁFICA (ZONAS Y PUEBLOS) +// ========================= +app.get("/provinces", authMiddleware, async (req, res) => { + try { const q = await pool.query("SELECT * FROM provinces ORDER BY name ASC"); res.json({ ok: true, provinces: q.rows }); } catch (e) { res.status(500).json({ ok: false }); } +}); + +app.get("/provinces/:id/towns", authMiddleware, async (req, res) => { + try { + const q = await pool.query("SELECT * FROM towns WHERE province_id=$1 ORDER BY name ASC", [req.params.id]); + res.json({ ok: true, towns: q.rows }); + } catch (e) { res.status(500).json({ ok: false }); } +}); app.get("/zones", authMiddleware, async (req, res) => { try { @@ -307,7 +323,9 @@ app.post("/zones", authMiddleware, async (req, res) => { } catch (e) { await client.query('ROLLBACK'); res.status(500).json({ ok: false }); } finally { client.release(); } }); -app.delete("/zones/:id", authMiddleware, async (req, res) => { try { await pool.query("DELETE FROM zones WHERE id=$1 AND owner_id=$2", [req.params.id, req.user.accountId]); res.json({ ok: true }); } catch (e) { res.status(500).json({ ok: false }); } }); +app.delete("/zones/:id", authMiddleware, async (req, res) => { + try { await pool.query("DELETE FROM zones WHERE id=$1 AND owner_id=$2", [req.params.id, req.user.accountId]); res.json({ ok: true }); } catch (e) { res.status(500).json({ ok: false }); } +}); app.get("/zones/:id/operators", authMiddleware, async (req, res) => { try { const q = await pool.query("SELECT user_id FROM user_zones WHERE zone_id=$1", [req.params.id]); const assignedIds = q.rows.map(r => r.user_id); res.json({ ok: true, assignedIds }); } catch (e) { res.status(500).json({ ok: false }); } }); app.post("/zones/:id/assign", authMiddleware, async (req, res) => { @@ -326,7 +344,6 @@ app.post("/zones/:id/assign", authMiddleware, async (req, res) => { } catch (e) { await client.query('ROLLBACK'); res.status(500).json({ ok: false }); } finally { client.release(); } }); -// AUTO-ASIGNACIÓN app.get("/towns/:id/auto-assign", authMiddleware, async (req, res) => { try { const qZone = await pool.query(`