Actualizar server.js
This commit is contained in:
61
server.js
61
server.js
@@ -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(`
|
||||||
|
|||||||
Reference in New Issue
Block a user