import express from "express"; import cors from "cors"; import bcrypt from "bcryptjs"; import jwt from "jsonwebtoken"; import pg from "pg"; const { Pool } = pg; const app = express(); // --- MIDDLEWARES --- app.use(cors()); app.use(express.json()); // --- ENV --- const { DATABASE_URL, JWT_SECRET, EVOLUTION_BASE_URL, EVOLUTION_API_KEY, EVOLUTION_INSTANCE, } = process.env; if (!DATABASE_URL || !JWT_SECRET) { console.error("ERROR FATAL: Faltan DATABASE_URL o JWT_SECRET"); process.exit(1); } // --- DB --- const pool = new Pool({ connectionString: DATABASE_URL, ssl: DATABASE_URL.includes("localhost") ? false : { rejectUnauthorized: false } }); // --- RUTA HEALTH CHECK (Para evitar el 404/502) --- app.get("/", (req, res) => { res.status(200).send("IntegraRepara API Online"); }); // --- 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 genCode6() { return String(Math.floor(100000 + Math.random() * 900000)); } function signToken(user) { return jwt.sign( { sub: user.id, phone: user.phone, email: user.email }, JWT_SECRET, { expiresIn: "30d" } ); } // --- WHATSAPP --- async function sendWhatsAppCode(phone, code) { if (!EVOLUTION_BASE_URL || !EVOLUTION_API_KEY) { console.log("Simulando envío WhatsApp (Faltan claves):", code); return { ok: true }; } try { // Detectamos si la URL base termina en / o no const baseUrl = EVOLUTION_BASE_URL.replace(/\/$/, ""); const url = `${baseUrl}/message/sendText/${EVOLUTION_INSTANCE}`; const body = { number: phone.replace("+", ""), text: `Tu código es: ${code}` }; const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", "apikey": EVOLUTION_API_KEY }, body: JSON.stringify(body) }); if (!res.ok) { const txt = await res.text(); console.error("Error Evolution:", txt); return { ok: false }; } return { ok: true }; } catch (e) { console.error("Error envío:", e); return { ok: false }; } } // --- RUTAS --- // Registro 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({error: "Faltan datos"}); const passwordHash = await bcrypt.hash(password, 10); await client.query('BEGIN'); // Check usuario const check = await client.query("SELECT * FROM users WHERE email=$1 OR phone=$2", [email, p]); let userId; if (check.rowCount > 0) { const u = check.rows[0]; if (u.is_verified) { await client.query('ROLLBACK'); return res.status(409).json({error: "Usuario ya existe"}); } // Actualizar existente no verificado await client.query("UPDATE users SET full_name=$1, password_hash=$2 WHERE id=$3", [fullName, passwordHash, u.id]); userId = u.id; } else { // Nuevo const ins = 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]); userId = ins.rows[0].id; } // Código const code = genCode6(); const hash = await bcrypt.hash(code, 10); const exp = new Date(Date.now() + 600000); await client.query("DELETE FROM login_codes WHERE user_id=$1", [userId]); await client.query("INSERT INTO login_codes (user_id, phone, code_hash, expires_at) VALUES ($1,$2,$3,$4)", [userId, p, hash, exp]); 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({error: "Error servidor"}); } finally { client.release(); } }); // Verificación 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.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 (q.rowCount === 0) return res.status(400).json({error: "Código inválido"}); const valid = await bcrypt.compare(String(code), q.rows[0].code_hash); if (!valid) return res.status(400).json({error: "Código incorrecto"}); await pool.query("UPDATE login_codes SET consumed_at=NOW() WHERE id=$1", [q.rows[0].id]); await pool.query("UPDATE users SET is_verified=TRUE WHERE id=$1", [q.rows[0].uid]); const token = signToken({id: q.rows[0].uid, email: q.rows[0].email, phone: p}); res.json({ok: true, token}); } catch (e) { console.error(e); res.status(500).json({error: "Error verify"}); } }); // Login 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({error: "Credenciales"}); const u = q.rows[0]; const match = await bcrypt.compare(password, u.password_hash); if (!match) return res.status(401).json({error: "Credenciales"}); if (!u.is_verified) return res.status(403).json({error: "No verificado"}); res.json({ok: true, token: signToken(u)}); } catch(e) { res.status(500).json({error: "Error login"}); } }); const port = process.env.PORT || 3000; app.listen(port, "0.0.0.0", () => console.log(`🚀 Server OK en puerto ${port}`));