Actualizar server.js

This commit is contained in:
2026-02-12 21:56:13 +00:00
parent 8d20ffc41c
commit 4d4dee84be

View File

@@ -16,6 +16,7 @@ const {
JWT_SECRET, JWT_SECRET,
EVOLUTION_BASE_URL, EVOLUTION_BASE_URL,
EVOLUTION_API_KEY, EVOLUTION_API_KEY,
EVOLUTION_INSTANCE, // <--- ¡FALTABA ESTO! (La instancia que envía los códigos)
} = process.env; } = process.env;
// --- DIAGNÓSTICO DE INICIO --- // --- DIAGNÓSTICO DE INICIO ---
@@ -24,17 +25,19 @@ console.log("🔧 INICIANDO SERVIDOR INTEGRA REPARA");
console.log("------------------------------------------------"); console.log("------------------------------------------------");
if (!DATABASE_URL) console.error("❌ FALTA: DATABASE_URL"); if (!DATABASE_URL) console.error("❌ FALTA: DATABASE_URL");
if (!JWT_SECRET) console.error("❌ FALTA: JWT_SECRET"); if (!JWT_SECRET) console.error("❌ FALTA: JWT_SECRET");
if (!EVOLUTION_BASE_URL) { if (!EVOLUTION_BASE_URL) {
console.error("⚠️ AVISO: No has puesto EVOLUTION_BASE_URL (WhatsApp no funcionará)"); console.error("⚠️ AVISO: No has puesto EVOLUTION_BASE_URL");
} else { } else {
console.log("✅ Evolution URL detectada:", EVOLUTION_BASE_URL); console.log("✅ Evolution URL:", EVOLUTION_BASE_URL);
} }
if (!EVOLUTION_API_KEY) {
console.error("⚠️ AVISO: No has puesto EVOLUTION_API_KEY (WhatsApp no funcionará)"); if (!EVOLUTION_INSTANCE) {
console.error("⚠️ AVISO: No has puesto EVOLUTION_INSTANCE (No se enviarán códigos de registro)");
} else { } else {
// Mostramos solo los primeros 4 caracteres por seguridad console.log("✅ Instancia de Notificaciones:", EVOLUTION_INSTANCE);
console.log("✅ Evolution API Key detectada:", EVOLUTION_API_KEY.substring(0, 4) + "...");
} }
console.log("------------------------------------------------"); console.log("------------------------------------------------");
if (!DATABASE_URL || !JWT_SECRET) { if (!DATABASE_URL || !JWT_SECRET) {
@@ -207,26 +210,61 @@ async function autoUpdateDB() {
// HELPERS // HELPERS
function normalizePhone(phone) { let p = String(phone || "").trim().replace(/\s+/g, "").replace(/-/g, ""); if (!p) return ""; if (!p.startsWith("+") && /^[6789]\d{8}$/.test(p)) return "+34" + p; return p; } function normalizePhone(phone) { let p = String(phone || "").trim().replace(/\s+/g, "").replace(/-/g, ""); if (!p) return ""; if (!p.startsWith("+") && /^[6789]\d{8}$/.test(p)) return "+34" + p; return p; }
function genCode6() { return String(Math.floor(100000 + Math.random() * 900000)); }
function signToken(user) { const accountId = user.owner_id || user.id; return jwt.sign({ sub: user.id, email: user.email, phone: user.phone, role: user.role || 'operario', accountId }, JWT_SECRET, { expiresIn: "30d" }); } function signToken(user) { const accountId = user.owner_id || user.id; return jwt.sign({ sub: user.id, email: user.email, phone: user.phone, role: user.role || 'operario', accountId }, JWT_SECRET, { expiresIn: "30d" }); }
function authMiddleware(req, res, next) { const h = req.headers.authorization || ""; const token = h.startsWith("Bearer ") ? h.slice(7) : ""; if (!token) return res.status(401).json({ ok: false, error: "No token" }); try { req.user = jwt.verify(token, JWT_SECRET); next(); } catch { return res.status(401).json({ ok: false, error: "Token inválido" }); } } function authMiddleware(req, res, next) { const h = req.headers.authorization || ""; const token = h.startsWith("Bearer ") ? h.slice(7) : ""; if (!token) return res.status(401).json({ ok: false, error: "No token" }); try { req.user = jwt.verify(token, JWT_SECRET); next(); } catch { return res.status(401).json({ ok: false, error: "Token inválido" }); } }
function genCode6() { return String(Math.floor(100000 + Math.random() * 900000)); }
// --- WHATSAPP HELPER (Automatización) --- // --- FUNCIÓN DE ENVÍO DE CÓDIGO (REGISTRO) ---
async function sendWhatsAppCode(phone, code) {
if (!EVOLUTION_BASE_URL || !EVOLUTION_API_KEY || !EVOLUTION_INSTANCE) {
console.error("❌ ERROR: Faltan variables para enviar WhatsApp (URL, APIKEY o INSTANCE)");
return;
}
// Aseguramos que la URL no tenga barra al final y añadimos la instancia
const url = `${EVOLUTION_BASE_URL.replace(/\/$/, "")}/message/sendText/${EVOLUTION_INSTANCE}`;
const number = phone.replace("+", ""); // Quitar el + para la API
console.log(`📤 Enviando código a ${number} desde instancia ${EVOLUTION_INSTANCE}...`);
try {
const res = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"apikey": EVOLUTION_API_KEY
},
body: JSON.stringify({
number: number,
text: `🔐 Código de verificación IntegraRepara: *${code}*`
})
});
if (!res.ok) {
const err = await res.text();
console.error("❌ Error enviando WhatsApp:", res.status, err);
} else {
console.log("✅ WhatsApp enviado correctamente.");
}
} catch (e) {
console.error("❌ Excepción enviando WhatsApp:", e.message);
}
}
// --- AUTOMATIZACIÓN DE INSTANCIAS (CLIENTES) ---
async function ensureInstance(instanceName) { async function ensureInstance(instanceName) {
if (!EVOLUTION_BASE_URL || !EVOLUTION_API_KEY) throw new Error("Faltan variables EVOLUTION en el servidor"); if (!EVOLUTION_BASE_URL || !EVOLUTION_API_KEY) throw new Error("Faltan variables EVOLUTION en el servidor");
const baseUrl = EVOLUTION_BASE_URL.replace(/\/$/, ""); const baseUrl = EVOLUTION_BASE_URL.replace(/\/$/, "");
const headers = { const headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
"apikey": EVOLUTION_API_KEY.trim() // Trim por si se copió con espacios "apikey": EVOLUTION_API_KEY.trim()
}; };
// 1. Verificar si existe (intentando pedir estado)
const checkRes = await fetch(`${baseUrl}/instance/connectionState/${instanceName}`, { headers }); const checkRes = await fetch(`${baseUrl}/instance/connectionState/${instanceName}`, { headers });
if (checkRes.status === 404) { if (checkRes.status === 404) {
console.log(`🚀 Creando instancia automática: ${instanceName}`); console.log(`🚀 Creando instancia automática: ${instanceName}`);
// 2. Si no existe, crear
const createRes = await fetch(`${baseUrl}/instance/create`, { const createRes = await fetch(`${baseUrl}/instance/create`, {
method: 'POST', method: 'POST',
headers, headers,
@@ -238,24 +276,22 @@ async function ensureInstance(instanceName) {
}); });
if (!createRes.ok) { if (!createRes.ok) {
// Si falla, leemos el error para mostrarlo en el log
const errText = await createRes.text(); const errText = await createRes.text();
console.error("❌ Error Evolution API al crear:", createRes.status, errText); if (createRes.status === 401) throw new Error("API KEY INCORRECTA");
if (createRes.status === 401) {
throw new Error("API KEY INCORRECTA: Revisa EVOLUTION_API_KEY en las variables de entorno.");
}
throw new Error(`Error creando instancia: ${errText}`); throw new Error(`Error creando instancia: ${errText}`);
} }
} else if (checkRes.status === 401) { } else if (checkRes.status === 401) {
throw new Error("API KEY INCORRECTA: Revisa EVOLUTION_API_KEY en las variables de entorno."); throw new Error("API KEY INCORRECTA");
} }
return { baseUrl, headers }; return { baseUrl, headers };
} }
// RUTAS AUTH // RUTAS AUTH
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) VALUES ($1, $2, $3, $4, $5, $6, 'admin', NULL) 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]); 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/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) VALUES ($1, $2, $3, $4, $5, $6, 'admin', NULL) 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]);
// ENVÍO DE WHATSAPP
await sendWhatsAppCode(p, code);
await client.query('COMMIT'); res.json({ ok: true, phone: p }); } catch (e) { await client.query('ROLLBACK'); console.error(e); 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 > 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/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 }); } });
@@ -264,23 +300,17 @@ app.post("/auth/login", async (req, res) => { try { const { email, password } =
// ========================================== // ==========================================
app.get("/whatsapp/status", authMiddleware, async (req, res) => { app.get("/whatsapp/status", authMiddleware, async (req, res) => {
try { try {
// Generar nombre único para la instancia: "cliente_123"
const instanceName = `cliente_${req.user.accountId}`; const instanceName = `cliente_${req.user.accountId}`;
const { baseUrl, headers } = await ensureInstance(instanceName); const { baseUrl, headers } = await ensureInstance(instanceName);
// 1. Obtener estado
const stateRes = await fetch(`${baseUrl}/instance/connectionState/${instanceName}`, { headers }); const stateRes = await fetch(`${baseUrl}/instance/connectionState/${instanceName}`, { headers });
const stateData = await stateRes.json(); const stateData = await stateRes.json();
const state = stateData.instance?.state || "close"; const state = stateData.instance?.state || "close";
// 2. Si no conectado, obtener QR
let qr = null; let qr = null;
if (state !== "open") { if (state !== "open") {
const qrRes = await fetch(`${baseUrl}/instance/connect/${instanceName}`, { headers }); const qrRes = await fetch(`${baseUrl}/instance/connect/${instanceName}`, { headers });
const qrData = await qrRes.json(); const qrData = await qrRes.json();
qr = qrData.code || qrData.base64; qr = qrData.code || qrData.base64;
} }
res.json({ ok: true, state, qr, instanceName }); res.json({ ok: true, state, qr, instanceName });
} catch (e) { } catch (e) {
console.error("Error WhatsApp EndPoint:", e.message); console.error("Error WhatsApp EndPoint:", e.message);