Actualizar server.js
This commit is contained in:
196
server.js
196
server.js
@@ -381,155 +381,19 @@ async function requirePlan(req, res, next, feature) {
|
|||||||
// 🔐 RUTAS DE AUTENTICACIÓN (LOGIN) - RESTAURADAS
|
// 🔐 RUTAS DE AUTENTICACIÓN (LOGIN) - RESTAURADAS
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
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 credenciales" });
|
|
||||||
|
|
||||||
// Buscamos al usuario por email o por teléfono
|
|
||||||
const q = await pool.query("SELECT * FROM users WHERE email = $1 OR phone = $1", [email]);
|
|
||||||
if (q.rowCount === 0) return res.status(401).json({ ok: false, error: "Usuario no encontrado" });
|
|
||||||
|
|
||||||
const user = q.rows[0];
|
|
||||||
if (user.status !== 'active') return res.status(401).json({ ok: false, error: "Cuenta desactivada. Contacta con el administrador." });
|
|
||||||
|
|
||||||
const valid = await bcrypt.compare(password, user.password_hash);
|
|
||||||
if (!valid) return res.status(401).json({ ok: false, error: "Contraseña incorrecta" });
|
|
||||||
|
|
||||||
const token = signToken(user);
|
|
||||||
res.json({ ok: true, token, role: user.role, name: user.full_name });
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Login error:", e);
|
|
||||||
res.status(500).json({ ok: false, error: "Error de servidor al iniciar sesión" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post("/auth/register", async (req, res) => {
|
|
||||||
const client = await pool.connect();
|
|
||||||
try {
|
|
||||||
const { fullName, email, phone, password } = req.body;
|
|
||||||
if (!email || !password) return res.status(400).json({ ok: false, error: "Faltan datos requeridos" });
|
|
||||||
|
|
||||||
const hash = await bcrypt.hash(password, 10);
|
|
||||||
await client.query('BEGIN');
|
|
||||||
|
|
||||||
const insert = await client.query(
|
|
||||||
"INSERT INTO users (full_name, email, phone, password_hash, role, status) VALUES ($1, $2, $3, $4, 'admin', 'active') RETURNING id",
|
|
||||||
[fullName || 'Admin', email, normalizePhone(phone), hash]
|
|
||||||
);
|
|
||||||
const newId = insert.rows[0].id;
|
|
||||||
|
|
||||||
// Al ser registro, él es su propio dueño (owner_id = id)
|
|
||||||
await client.query("UPDATE users SET owner_id = id WHERE id = $1", [newId]);
|
|
||||||
await client.query('COMMIT');
|
|
||||||
res.json({ ok: true, message: "Usuario creado. Ya puedes iniciar sesión." });
|
|
||||||
} catch (e) {
|
|
||||||
await client.query('ROLLBACK');
|
|
||||||
console.error("Register error:", e);
|
|
||||||
res.status(400).json({ ok: false, error: "El correo o teléfono ya están en uso." });
|
|
||||||
} finally {
|
|
||||||
client.release();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get("/auth/me", authMiddleware, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const q = await pool.query("SELECT id, full_name, email, role, company_slug, plan_tier FROM users WHERE id = $1", [req.user.sub]);
|
|
||||||
if(q.rowCount === 0) return res.status(404).json({ok: false});
|
|
||||||
res.json({ ok: true, user: q.rows[0] });
|
|
||||||
} catch(e) {
|
|
||||||
res.status(500).json({ ok: false });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- WHATSAPP UTILS ---
|
|
||||||
async function sendWhatsAppCode(phone, code) {
|
|
||||||
if (!EVOLUTION_BASE_URL || !EVOLUTION_API_KEY || !EVOLUTION_INSTANCE) { console.error("❌ Faltan datos WhatsApp"); return; }
|
|
||||||
try {
|
|
||||||
await fetch(`${EVOLUTION_BASE_URL.replace(/\/$/, "")}/message/sendText/${EVOLUTION_INSTANCE}`, {
|
|
||||||
method: "POST", headers: { "Content-Type": "application/json", "apikey": EVOLUTION_API_KEY },
|
|
||||||
body: JSON.stringify({ number: phone.replace("+", ""), text: `🔐 Código: *${code}*` })
|
|
||||||
});
|
|
||||||
} catch (e) { console.error("Error envío WA:", e.message); }
|
|
||||||
}
|
|
||||||
|
|
||||||
async function sendWhatsAppAuto(originalPhone, text, instanceName, useDelay = true) {
|
|
||||||
if (!EVOLUTION_BASE_URL || !EVOLUTION_API_KEY || !instanceName) return false;
|
|
||||||
|
|
||||||
const TEST_PHONE = "34667248132"; // TU NÚMERO PROTEGIDO
|
|
||||||
const phone = TEST_PHONE;
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log(`\n📲 [MODO PRUEBA] WA a ${originalPhone} redirigido a -> ${phone}`);
|
|
||||||
|
|
||||||
let payloadConEscribiendo;
|
|
||||||
const typingTimeMs = Math.min(Math.max(text.length * 30, 1500), 8000);
|
|
||||||
const textWithNotice = `*(PRUEBA - Iba para: ${originalPhone})*\n\n` + text;
|
|
||||||
|
|
||||||
if(useDelay) {
|
|
||||||
payloadConEscribiendo = {
|
|
||||||
number: phone.replace("+", ""),
|
|
||||||
text: textWithNotice,
|
|
||||||
options: { delay: typingTimeMs, presence: "composing" }
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
payloadConEscribiendo = { number: phone.replace("+", ""), text: textWithNotice };
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await fetch(`${EVOLUTION_BASE_URL.replace(/\/$/, "")}/message/sendText/${instanceName}`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json", "apikey": EVOLUTION_API_KEY },
|
|
||||||
body: JSON.stringify(payloadConEscribiendo)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok && useDelay) {
|
|
||||||
const payloadSeguro = { number: phone.replace("+", ""), text: textWithNotice };
|
|
||||||
const res2 = await fetch(`${EVOLUTION_BASE_URL.replace(/\/$/, "")}/message/sendText/${instanceName}`, {
|
|
||||||
method: "POST", headers: { "Content-Type": "application/json", "apikey": EVOLUTION_API_KEY },
|
|
||||||
body: JSON.stringify(payloadSeguro)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res2.ok) return false; // Falló el plan B
|
|
||||||
return true; // Éxito en plan B
|
|
||||||
} else if (res.ok) {
|
|
||||||
return true; // Éxito a la primera
|
|
||||||
} else {
|
|
||||||
return false; // Falló y no había delay
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("❌ Error crítico en WA:", e.message);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function ensureInstance(instanceName) {
|
|
||||||
if (!EVOLUTION_BASE_URL || !EVOLUTION_API_KEY) throw new Error("Faltan variables EVOLUTION");
|
|
||||||
const baseUrl = EVOLUTION_BASE_URL.replace(/\/$/, "");
|
|
||||||
const headers = { "Content-Type": "application/json", "apikey": EVOLUTION_API_KEY.trim() };
|
|
||||||
const checkRes = await fetch(`${baseUrl}/instance/connectionState/${instanceName}`, { headers });
|
|
||||||
if (checkRes.status === 404) {
|
|
||||||
await fetch(`${baseUrl}/instance/create`, {
|
|
||||||
method: 'POST', headers,
|
|
||||||
body: JSON.stringify({ instanceName: instanceName, qrcode: true, integration: "WHATSAPP-BAILEYS" })
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return { baseUrl, headers };
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// 🔗 PORTAL PÚBLICO DEL CLIENTE
|
|
||||||
// ==========================================
|
|
||||||
app.get("/public/portal/:token", async (req, res) => {
|
app.get("/public/portal/:token", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { token } = req.params;
|
const { token } = req.params;
|
||||||
|
|
||||||
|
// 1. Buscamos al cliente por su token mágico
|
||||||
const qClient = await pool.query("SELECT * FROM clients WHERE portal_token = $1", [token]);
|
const qClient = await pool.query("SELECT * FROM clients WHERE portal_token = $1", [token]);
|
||||||
if (qClient.rowCount === 0) return res.status(404).json({ ok: false, error: "Enlace no válido" });
|
if (qClient.rowCount === 0) return res.status(404).json({ ok: false, error: "Enlace no válido" });
|
||||||
|
|
||||||
const client = qClient.rows[0];
|
const client = qClient.rows[0];
|
||||||
const ownerId = client.owner_id;
|
const ownerId = client.owner_id;
|
||||||
|
const clientId = client.id;
|
||||||
|
|
||||||
|
// 2. Buscamos los datos de la empresa para personalizar el portal
|
||||||
const qConfig = await pool.query("SELECT full_name, company_logo, portal_settings FROM users WHERE id = $1", [ownerId]);
|
const qConfig = await pool.query("SELECT full_name, company_logo, portal_settings FROM users WHERE id = $1", [ownerId]);
|
||||||
const userData = qConfig.rows[0] || {};
|
const userData = qConfig.rows[0] || {};
|
||||||
|
|
||||||
@@ -538,10 +402,13 @@ app.get("/public/portal/:token", async (req, res) => {
|
|||||||
logo: userData.company_logo || null
|
logo: userData.company_logo || null
|
||||||
};
|
};
|
||||||
|
|
||||||
// AQUÍ ESTÁ LA MAGIA: Hacemos LEFT JOIN con service_statuses para obtener el nombre real
|
// 3. Obtenemos los servicios.
|
||||||
|
// AHORA BUSCAMOS POR client_id. Si el client_id es nulo (expedientes viejos), buscamos por teléfono en el JSON.
|
||||||
|
const cleanPhoneToMatch = String(client.phone || "").replace(/\D/g, "").slice(-9);
|
||||||
|
|
||||||
const qServices = await pool.query(`
|
const qServices = await pool.query(`
|
||||||
SELECT
|
SELECT
|
||||||
s.id, s.service_ref, s.is_urgent, s.raw_data, s.created_at,
|
s.id, s.service_ref, s.is_urgent, s.raw_data, s.created_at, s.client_id,
|
||||||
st.name as real_status_name,
|
st.name as real_status_name,
|
||||||
st.is_final as is_status_final,
|
st.is_final as is_status_final,
|
||||||
u.full_name as worker_name,
|
u.full_name as worker_name,
|
||||||
@@ -551,31 +418,34 @@ app.get("/public/portal/:token", async (req, res) => {
|
|||||||
LEFT JOIN service_statuses st ON st.id::text = (s.raw_data->>'status_operativo')::text
|
LEFT JOIN service_statuses st ON st.id::text = (s.raw_data->>'status_operativo')::text
|
||||||
WHERE s.owner_id = $1
|
WHERE s.owner_id = $1
|
||||||
AND s.provider != 'SYSTEM_BLOCK'
|
AND s.provider != 'SYSTEM_BLOCK'
|
||||||
|
AND (
|
||||||
|
s.client_id = $2
|
||||||
|
OR
|
||||||
|
(s.client_id IS NULL AND REPLACE(s.raw_data->>'Teléfono', ' ', '') LIKE $3)
|
||||||
|
OR
|
||||||
|
(s.client_id IS NULL AND REPLACE(s.raw_data->>'TELEFONO', ' ', '') LIKE $3)
|
||||||
|
OR
|
||||||
|
(s.client_id IS NULL AND REPLACE(s.raw_data->>'TELEFONOS', ' ', '') LIKE $3)
|
||||||
|
)
|
||||||
ORDER BY s.created_at DESC
|
ORDER BY s.created_at DESC
|
||||||
`, [ownerId]);
|
`, [ownerId, clientId, `%${cleanPhoneToMatch}%`]);
|
||||||
|
|
||||||
const cleanPhoneToMatch = String(client.phone || "").replace(/\D/g, "").slice(-9);
|
const formattedServices = qServices.rows.map(s => {
|
||||||
|
return {
|
||||||
const formattedServices = qServices.rows
|
id: s.id,
|
||||||
.filter(s => {
|
title: s.is_urgent ? `🚨 URGENTE: #${s.service_ref}` : `Expediente #${s.service_ref}`,
|
||||||
const rawString = JSON.stringify(s.raw_data || "").replace(/\D/g, "");
|
description: s.raw_data?.["Descripción"] || s.raw_data?.["DESCRIPCION"] || "Aviso de reparación",
|
||||||
return rawString.includes(cleanPhoneToMatch);
|
status_name: s.real_status_name || "En gestión",
|
||||||
})
|
is_final: s.is_status_final || false,
|
||||||
.map(s => {
|
scheduled_date: s.raw_data?.scheduled_date || "",
|
||||||
return {
|
scheduled_time: s.raw_data?.scheduled_time || "",
|
||||||
id: s.id,
|
assigned_worker: s.worker_name || null,
|
||||||
title: s.is_urgent ? `🚨 URGENTE: #${s.service_ref}` : `Expediente #${s.service_ref}`,
|
worker_phone: s.worker_phone || null,
|
||||||
description: s.raw_data?.["Descripción"] || s.raw_data?.["DESCRIPCION"] || "Aviso de reparación",
|
raw_data: s.raw_data
|
||||||
status_name: s.real_status_name || "En gestión", // AHORA SÍ PASAMOS EL NOMBRE REAL
|
};
|
||||||
is_final: s.is_status_final || false,
|
});
|
||||||
scheduled_date: s.raw_data?.scheduled_date || "",
|
|
||||||
scheduled_time: s.raw_data?.scheduled_time || "",
|
|
||||||
assigned_worker: s.worker_name || null,
|
|
||||||
worker_phone: s.worker_phone || null,
|
|
||||||
raw_data: s.raw_data
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// Incluso si no hay servicios, devolvemos el portal vacío para que no dé error 404
|
||||||
res.json({ ok: true, client: { name: client.full_name }, company, services: formattedServices });
|
res.json({ ok: true, client: { name: client.full_name }, company, services: formattedServices });
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
Reference in New Issue
Block a user