+
-if (!DATABASE_URL || !JWT_SECRET) {
- console.error("❌ ERROR FATAL: Faltan variables de entorno");
- process.exit(1);
-}
+ + Configuración Avanzada +
-const { - DATABASE_URL, - JWT_SECRET, - EVOLUTION_BASE_URL, - EVOLUTION_API_KEY, - EVOLUTION_INSTANCE, -} = process.env; + +
-const pool = new Pool({
- connectionString: DATABASE_URL,
- ssl: false
-});
+
+
+
+
+
-// ==========================================
-// 🧠 AUTO-ACTUALIZACIÓN DB
-// ==========================================
-async function autoUpdateDB() {
- const client = await pool.connect();
- try {
- console.log("🔄 Verificando estructura DB...");
+
- await client.query(`
- CREATE TABLE IF NOT EXISTS users (
- id SERIAL PRIMARY KEY,
- full_name TEXT NOT NULL,
- phone TEXT NOT NULL,
- email TEXT NOT NULL,
- dni TEXT,
- address TEXT,
- password_hash TEXT NOT NULL,
- is_verified BOOLEAN DEFAULT FALSE,
- owner_id INT,
- role TEXT DEFAULT 'operario',
- created_at TIMESTAMP DEFAULT NOW()
- );
- CREATE TABLE IF NOT EXISTS login_codes (
- id SERIAL PRIMARY KEY,
- user_id INT REFERENCES users(id) ON DELETE CASCADE,
- phone TEXT NOT NULL,
- code_hash TEXT NOT NULL,
- purpose TEXT DEFAULT 'register_verify',
- consumed_at TIMESTAMP,
- expires_at TIMESTAMP NOT NULL,
- created_at TIMESTAMP DEFAULT NOW()
- );
- CREATE TABLE IF NOT EXISTS guilds (
- id SERIAL PRIMARY KEY,
- owner_id INT REFERENCES users(id) ON DELETE CASCADE,
- name TEXT NOT NULL,
- created_at TIMESTAMP DEFAULT NOW()
- );
- CREATE TABLE IF NOT EXISTS user_guilds (
- user_id INT REFERENCES users(id) ON DELETE CASCADE,
- guild_id INT REFERENCES guilds(id) ON DELETE CASCADE,
- PRIMARY KEY (user_id, guild_id)
- );
- CREATE TABLE IF NOT EXISTS companies (
- id SERIAL PRIMARY KEY,
- owner_id INT REFERENCES users(id) ON DELETE CASCADE,
- name TEXT NOT NULL,
- cif TEXT,
- email TEXT,
- phone TEXT,
- address TEXT,
- created_at TIMESTAMP DEFAULT NOW()
- );
- CREATE TABLE IF NOT EXISTS clients (
- id SERIAL PRIMARY KEY,
- owner_id INT REFERENCES users(id) ON DELETE CASCADE,
- full_name TEXT NOT NULL,
- phone TEXT NOT NULL,
- email TEXT,
- addresses JSONB DEFAULT '[]',
- notes TEXT,
- created_at TIMESTAMP DEFAULT NOW()
- );
- CREATE TABLE IF NOT EXISTS service_statuses (
- id SERIAL PRIMARY KEY,
- owner_id INT REFERENCES users(id) ON DELETE CASCADE,
- name TEXT NOT NULL,
- color TEXT DEFAULT 'gray',
- is_default BOOLEAN DEFAULT FALSE,
- is_final BOOLEAN DEFAULT FALSE,
- created_at TIMESTAMP DEFAULT NOW()
- );
-
- -- Tablas de Zonas (Simplificadas)
- CREATE TABLE IF NOT EXISTS zones (
- id SERIAL PRIMARY KEY,
- name TEXT NOT NULL,
- owner_id INT,
- created_at TIMESTAMP DEFAULT NOW()
- );
- CREATE TABLE IF NOT EXISTS user_zones (
- user_id INT REFERENCES users(id) ON DELETE CASCADE,
- zone_id INT REFERENCES zones(id) ON DELETE CASCADE,
- PRIMARY KEY (user_id, zone_id)
- );
+
- CREATE TABLE IF NOT EXISTS services (
- id SERIAL PRIMARY KEY,
- owner_id INT REFERENCES users(id) ON DELETE CASCADE,
- client_id INT REFERENCES clients(id) ON DELETE SET NULL,
- status_id INT REFERENCES service_statuses(id) ON DELETE SET NULL,
- guild_id INT REFERENCES guilds(id) ON DELETE SET NULL,
- assigned_to INT REFERENCES users(id) ON DELETE SET NULL,
- title TEXT,
- description TEXT,
- contact_phone TEXT,
- contact_name TEXT,
- address TEXT,
- email TEXT,
- scheduled_date DATE DEFAULT CURRENT_DATE,
- scheduled_time TIME DEFAULT CURRENT_TIME,
- duration_minutes INT DEFAULT 30,
- is_urgent BOOLEAN DEFAULT FALSE,
- is_company BOOLEAN DEFAULT FALSE,
- company_id INT REFERENCES companies(id) ON DELETE SET NULL,
- company_ref TEXT,
- internal_notes TEXT,
- client_notes TEXT,
- closed_at TIMESTAMP,
- created_at TIMESTAMP DEFAULT NOW()
- );
- CREATE TABLE IF NOT EXISTS service_logs (
- id SERIAL PRIMARY KEY,
- service_id INT REFERENCES services(id) ON DELETE CASCADE,
- user_id INT REFERENCES users(id) ON DELETE SET NULL,
- old_status_id INT REFERENCES service_statuses(id),
- new_status_id INT REFERENCES service_statuses(id),
- comment TEXT,
- created_at TIMESTAMP DEFAULT NOW()
- );
- `);
+
-// 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 sendWhatsAppCode(p, code); 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/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/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();} });
+
+
+
+ Gestor de Plantillas
+Próximamente podrás editar tus textos aquí.
+