Actualizar server.js

This commit is contained in:
2026-02-08 23:17:20 +00:00
parent 9cdf4cb548
commit 1663578f3e

View File

@@ -3,6 +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";
// NOTA: No importamos node-fetch. Usamos el nativo de Node v18+.
const { Pool } = pg; const { Pool } = pg;
const app = express(); const app = express();
@@ -137,7 +138,7 @@ async function autoUpdateDB() {
); );
`); `);
// 3. SERVICIOS Y LOGS // 3. SERVICIOS
await client.query(` await client.query(`
CREATE TABLE IF NOT EXISTS services ( CREATE TABLE IF NOT EXISTS services (
id SERIAL PRIMARY KEY, 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(` await client.query(`
DO $$ BEGIN 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; 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 $$; END $$;
`); `);
// 5. CARGA MASIVA INE (AHORA SÍ)
await seedSpainData(client);
console.log("✅ DB Sincronizada."); console.log("✅ DB Sincronizada.");
} catch (e) { console.error("❌ Error DB:", e); } finally { client.release(); } } 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) { async function seedSpainData(client) {
try { 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"); 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"); 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(); 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"); await client.query("BEGIN");
@@ -230,31 +234,33 @@ async function seedSpainData(client) {
const provincesSet = new Set(); const provincesSet = new Set();
listFull.forEach(item => provincesSet.add(item.parent_op)); listFull.forEach(item => provincesSet.add(item.parent_op));
// Mapa para guardar el ID real de cada provincia
const provinceMap = new Map(); const provinceMap = new Map();
for (const provName of provincesSet) { 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]); 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]); 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) { for (const item of listFull) {
const pid = provinceMap.get(item.parent_op); const pid = provinceMap.get(item.parent_op);
if (pid) { 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("INSERT INTO towns (province_id, name) VALUES ($1, $2)", [pid, item.label]);
} }
} }
await client.query("COMMIT"); 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) { } catch (e) {
await client.query("ROLLBACK"); 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/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();} }); 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 }); } }); // API GEOGRÁFICA (ZONAS Y PUEBLOS)
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("/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) => { app.get("/zones", authMiddleware, async (req, res) => {
try { 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(); } } 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.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) => { 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(); } } 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) => { app.get("/towns/:id/auto-assign", authMiddleware, async (req, res) => {
try { try {
const qZone = await pool.query(` const qZone = await pool.query(`