From 2cf171c042941f06a0473717a27dce5b4790dd50 Mon Sep 17 00:00:00 2001 From: marsalva Date: Sat, 7 Feb 2026 21:24:40 +0000 Subject: [PATCH] Actualizar server.js --- server.js | 330 +++++++++++++++++++++++------------------------------- 1 file changed, 139 insertions(+), 191 deletions(-) diff --git a/server.js b/server.js index 72b4310..5982b40 100644 --- a/server.js +++ b/server.js @@ -3,15 +3,16 @@ import cors from "cors"; import bcrypt from "bcryptjs"; import jwt from "jsonwebtoken"; import pg from "pg"; +import fetch from "node-fetch"; // Asegúrate de tener node-fetch si usas Node < 18 const { Pool } = pg; - const app = express(); + app.use(cors()); app.use(express.json()); // ========================= -// ENV (Coolify variables) +// ENV // ========================= const { DATABASE_URL, @@ -19,36 +20,35 @@ const { EVOLUTION_BASE_URL, EVOLUTION_API_KEY, EVOLUTION_INSTANCE, - EVOLUTION_SENDER_NUMBER } = process.env; -if (!DATABASE_URL) console.warn("⚠️ DATABASE_URL no definido"); -if (!JWT_SECRET) console.warn("⚠️ JWT_SECRET no definido"); +if (!DATABASE_URL || !JWT_SECRET) { + console.error("❌ Faltan variables de entorno críticas (DATABASE_URL o JWT_SECRET)"); + process.exit(1); +} // ========================= -// DB +// DB CONNECTION // ========================= const pool = new Pool({ connectionString: DATABASE_URL, - ssl: DATABASE_URL?.includes("localhost") ? false : { rejectUnauthorized: false } + ssl: DATABASE_URL.includes("localhost") ? false : { rejectUnauthorized: false } }); // ========================= -// Helpers +// HELPERS // ========================= function normalizePhone(phone) { - // Ajusta a tu gusto (E.164). Para España: +34XXXXXXXXX - let p = String(phone || "").trim().replace(/\s+/g, ""); + let p = String(phone || "").trim().replace(/\s+/g, "").replace(/-/g, ""); if (!p) return ""; - if (!p.startsWith("+")) { - // si meten 600..., asumimos España - if (/^\d{9}$/.test(p)) p = "+34" + p; + // Lógica simple para España + if (!p.startsWith("+") && /^[6789]\d{8}$/.test(p)) { + return "+34" + p; } return p; } function genCode6() { - // 6 dígitos return String(Math.floor(100000 + Math.random() * 900000)); } @@ -60,6 +60,7 @@ function signToken(user) { ); } +// Middleware de autenticación function authMiddleware(req, res, next) { const h = req.headers.authorization || ""; const token = h.startsWith("Bearer ") ? h.slice(7) : ""; @@ -70,251 +71,198 @@ function authMiddleware(req, res, next) { req.user = payload; next(); } catch { - return res.status(401).json({ ok: false, error: "Token inválido" }); + return res.status(401).json({ ok: false, error: "Token inválido o expirado" }); } } -// Rate-limit muy simple en memoria (para no freír el WhatsApp) -const lastSendByPhone = new Map(); -function canSendCode(phone) { - const now = Date.now(); - const last = lastSendByPhone.get(phone) || 0; - // 1 envío cada 60s - if (now - last < 60_000) return false; - lastSendByPhone.set(phone, now); - return true; -} - // ========================= -// Evolution API (WhatsApp) +// EVOLUTION API (WHATSAPP) // ========================= async function sendWhatsAppCode(phone, code) { - if (!EVOLUTION_BASE_URL || !EVOLUTION_API_KEY || !EVOLUTION_INSTANCE) { - console.warn("⚠️ Evolution API env no configuradas, no envío WhatsApp."); - return { ok: false, skipped: true }; + if (!EVOLUTION_BASE_URL || !EVOLUTION_API_KEY) { + console.warn("⚠️ Evolution API no configurada. Código (Log):", code); + return { ok: true, skipped: true }; // Devolvemos true para no romper el flujo en desarrollo } - // OJO: el endpoint exacto puede variar según tu Evolution. - // Ajustamos a un patrón típico: /message/sendText/{instance} - const url = `${EVOLUTION_BASE_URL.replace(/\/$/, "")}/message/sendText/${encodeURIComponent(EVOLUTION_INSTANCE)}`; + try { + const url = `${EVOLUTION_BASE_URL.replace(/\/$/, "")}/message/sendText/${EVOLUTION_INSTANCE}`; + + const body = { + number: phone.replace("+", ""), // Evolution suele preferir el número sin el + + text: `🔐 Tu código de verificación IntegraRepara es: *${code}*\n\nNo lo compartas con nadie.` + }; - const payload = { - number: phone, // normalmente en formato +34... - text: `Tu código de verificación es: ${code}\nCaduca en 10 minutos.` - }; + const res = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + "apikey": EVOLUTION_API_KEY + }, + body: JSON.stringify(body) + }); - const res = await fetch(url, { - method: "POST", - headers: { - "Content-Type": "application/json", - "apikey": EVOLUTION_API_KEY - }, - body: JSON.stringify(payload) - }); - - const text = await res.text(); - let data; - try { data = JSON.parse(text); } catch { data = text; } - - if (!res.ok) { - console.error("❌ Evolution send failed:", res.status, data); - return { ok: false, status: res.status, data }; + const data = await res.json(); + + if (!res.ok) { + console.error("❌ Error Evolution:", data); + return { ok: false }; + } + return { ok: true }; + } catch (error) { + console.error("❌ Error enviando WhatsApp:", error); + return { ok: false }; } - return { ok: true, data }; } // ========================= -// Health -// ========================= -app.get("/health", async (req, res) => { - try { - const r = await pool.query("select now() as now"); - res.json({ ok: true, db: true, now: r.rows[0].now }); - } catch (e) { - res.json({ ok: false, db: false, error: String(e?.message || e) }); - } -}); - -app.get("/", (req, res) => res.send("IntegraRepara API OK")); - -// ========================= -// AUTH +// RUTAS DE AUTENTICACIÓN // ========================= -// Registro: crea user + manda código WhatsApp +// 1. REGISTRO app.post("/auth/register", async (req, res) => { + const client = await pool.connect(); try { - const { - fullName, - phone, - address, - dni, - email, - password - } = req.body || {}; - + const { fullName, phone, address, dni, email, password } = req.body; const p = normalizePhone(phone); - if (!fullName || !p || !address || !dni || !email || !password) { - return res.status(400).json({ ok: false, error: "Faltan campos obligatorios" }); + if (!fullName || !p || !email || !password) { + return res.status(400).json({ ok: false, error: "Faltan datos" }); } - if (!canSendCode(p)) { - return res.status(429).json({ ok: false, error: "Espera 1 minuto antes de pedir otro código" }); - } + const passwordHash = await bcrypt.hash(password, 10); + + await client.query('BEGIN'); - const passwordHash = await bcrypt.hash(String(password), 10); + // Verificamos si existe + const checkUser = await client.query("SELECT * FROM users WHERE email = $1 OR phone = $2", [email, p]); + + let userId; - // intenta crear usuario - let user; - try { - const q = await pool.query( - `insert into users (full_name, phone, address, dni, email, password_hash, is_verified) - values ($1,$2,$3,$4,$5,$6,false) - returning id, full_name, phone, email, is_verified`, - [String(fullName).trim(), p, String(address).trim(), String(dni).trim(), String(email).trim().toLowerCase(), passwordHash] + if (checkUser.rowCount > 0) { + const existing = checkUser.rows[0]; + if (existing.is_verified) { + await client.query('ROLLBACK'); + return res.status(409).json({ ok: false, error: "El usuario ya existe y está verificado. Por favor inicia sesión." }); + } else { + // Existe pero NO verificado: Actualizamos datos y reenvíamos código + await client.query( + `UPDATE users SET full_name=$1, address=$2, dni=$3, password_hash=$4 WHERE id=$5`, + [fullName, address, dni, passwordHash, existing.id] + ); + userId = existing.id; + } + } else { + // Nuevo usuario + const insert = await client.query( + `INSERT INTO users (full_name, phone, address, dni, email, password_hash) + VALUES ($1, $2, $3, $4, $5, $6) RETURNING id`, + [fullName, p, address, dni, email, passwordHash] ); - user = q.rows[0]; - } catch (e) { - // si ya existe por email o phone - return res.status(409).json({ ok: false, error: "Ya existe un usuario con ese teléfono o email" }); + userId = insert.rows[0].id; } + // Generar código const code = genCode6(); const codeHash = await bcrypt.hash(code, 10); const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 min - await pool.query( - `insert into login_codes (user_id, phone, code_hash, purpose, expires_at) - values ($1,$2,$3,'register_verify',$4)`, - [user.id, p, codeHash, expiresAt] + // Invalidar códigos anteriores + await client.query("DELETE FROM login_codes WHERE user_id = $1", [userId]); + + // Insertar nuevo código + await client.query( + `INSERT INTO login_codes (user_id, phone, code_hash, expires_at) VALUES ($1, $2, $3, $4)`, + [userId, p, codeHash, expiresAt] ); - await sendWhatsAppCode(p, code); + // Enviar WhatsApp (fuera de la transacción para no bloquear DB si tarda) + const whatsAppStatus = await sendWhatsAppCode(p, code); + + await client.query('COMMIT'); + + res.json({ ok: true, phone: p, msg: "Código enviado" }); - res.json({ ok: true, needsVerify: true, phone: p }); } catch (e) { - res.status(500).json({ ok: false, error: String(e?.message || e) }); + await client.query('ROLLBACK'); + console.error(e); + res.status(500).json({ ok: false, error: "Error interno del servidor" }); + } finally { + client.release(); } }); -// Verificar código de registro: activa user + devuelve token +// 2. VERIFICACIÓN app.post("/auth/verify", async (req, res) => { try { - const { phone, code } = req.body || {}; + const { phone, code } = req.body; const p = normalizePhone(phone); - if (!p || !code) return res.status(400).json({ ok: false, error: "Faltan phone o code" }); - - const u = await pool.query(`select * from users where phone=$1`, [p]); - if (!u.rowCount) return res.status(404).json({ ok: false, error: "Usuario no existe" }); - const user = u.rows[0]; - - // busca último código válido no consumido - const c = await pool.query( - `select * from login_codes - where phone=$1 and purpose='register_verify' and consumed_at is null and expires_at > now() - order by created_at desc - limit 1`, + // Buscar código válido + const codeQuery = await pool.query( + `SELECT lc.*, u.id as user_id, u.email, u.full_name, u.phone + 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 (!c.rowCount) return res.status(400).json({ ok: false, error: "Código inválido o caducado" }); + if (codeQuery.rowCount === 0) { + return res.status(400).json({ ok: false, error: "Código inválido o expirado" }); + } - const row = c.rows[0]; - const ok = await bcrypt.compare(String(code).trim(), row.code_hash); - if (!ok) return res.status(400).json({ ok: false, error: "Código incorrecto" }); + const row = codeQuery.rows[0]; + const valid = await bcrypt.compare(String(code), row.code_hash); - 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`, [user.id]); + if (!valid) { + return res.status(400).json({ ok: false, error: "Código incorrecto" }); + } - // settings por defecto - await pool.query( - `insert into user_settings (user_id, company_name) - values ($1,$2) - on conflict (user_id) do nothing`, - [user.id, user.full_name] - ); + // Consumir código y verificar usuario + 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.user_id]); + const user = { id: row.user_id, email: row.email, phone: row.phone }; const token = signToken(user); + res.json({ ok: true, token }); + } catch (e) { - res.status(500).json({ ok: false, error: String(e?.message || e) }); + console.error(e); + res.status(500).json({ ok: false, error: "Error verificando código" }); } }); -// Login normal (email+password). Si quieres 2FA por WhatsApp también, lo hacemos luego. +// 3. LOGIN app.post("/auth/login", async (req, res) => { try { - const { email, password } = req.body || {}; - if (!email || !password) return res.status(400).json({ ok: false, error: "Faltan email o password" }); + const { email, password } = req.body; + + const u = await pool.query("SELECT * FROM users WHERE email = $1", [email]); + if (u.rowCount === 0) return res.status(401).json({ ok: false, error: "Credenciales inválidas" }); - const q = await pool.query(`select * from users where email=$1`, [String(email).trim().toLowerCase()]); - if (!q.rowCount) return res.status(401).json({ ok: false, error: "Credenciales incorrectas" }); + const user = u.rows[0]; + const match = await bcrypt.compare(password, user.password_hash); - const user = q.rows[0]; + if (!match) return res.status(401).json({ ok: false, error: "Credenciales inválidas" }); - const ok = await bcrypt.compare(String(password), user.password_hash); - if (!ok) return res.status(401).json({ ok: false, error: "Credenciales incorrectas" }); - - if (!user.is_verified) return res.status(403).json({ ok: false, error: "Cuenta no verificada por WhatsApp" }); + if (!user.is_verified) { + return res.status(403).json({ ok: false, error: "Cuenta no verificada. Regístrate de nuevo para recibir el código." }); + } const token = signToken(user); res.json({ ok: true, token }); + } catch (e) { - res.status(500).json({ ok: false, error: String(e?.message || e) }); + res.status(500).json({ ok: false, error: "Error en login" }); } }); -// ========================= -// SERVICES (multi-tenant) -// ========================= +// 4. DATOS PROTEGIDOS (Dashboard) app.get("/services", authMiddleware, async (req, res) => { - try { - const userId = req.user.sub; - const q = await pool.query( - `select * from services where user_id=$1 order by created_at desc`, - [userId] - ); - res.json({ ok: true, count: q.rowCount, services: q.rows }); - } catch (e) { - res.status(500).json({ ok: false, error: String(e?.message || e) }); - } + const q = await pool.query("SELECT * FROM services WHERE user_id = $1 ORDER BY created_at DESC", [req.user.sub]); + res.json({ ok: true, services: q.rows }); }); -app.post("/services", authMiddleware, async (req, res) => { - try { - const userId = req.user.sub; - const body = req.body || {}; - const title = body.title; - const clientName = body.clientName ?? body.client ?? ""; - - if (!title) return res.status(400).json({ ok: false, error: "Falta title" }); - - const q = await pool.query( - `insert into services (user_id, title, client_name, phone, address, notes) - values ($1,$2,$3,$4,$5,$6) - returning *`, - [ - userId, - String(title).trim(), - clientName ? String(clientName).trim() : null, - body.phone ? String(body.phone).trim() : null, - body.address ? String(body.address).trim() : null, - body.notes ? String(body.notes).trim() : null - ] - ); - - res.status(201).json({ ok: true, service: q.rows[0] }); - } catch (e) { - res.status(500).json({ ok: false, error: String(e?.message || e) }); - } -}); - -// ========================= -// STATIC (HTML) -// ========================= -app.use(express.static("public")); - const port = process.env.PORT || 3000; -app.listen(port, () => console.log(`🚀 API listening on :${port}`)); \ No newline at end of file +app.listen(port, () => console.log(`🚀 Server en puerto ${port}`)); \ No newline at end of file