app V1 completada

This commit is contained in:
2025-09-26 19:55:12 -06:00
parent 7d7a845a75
commit ca605a5759
3 changed files with 287 additions and 3 deletions

View File

@@ -104,6 +104,136 @@ router.get('/requests.csv', (_req, res) => {
res.send(csv);
});
// Export CSV: users, devices, vlans
router.get('/users.csv', async (_req, res) => {
const items = await readUsersFromDb();
const cols = ['username','password','vlan','disabled','etiquetas','created_at','updated_at','habilitado_since'];
const esc = (v) => {
const s = v == null ? '' : Array.isArray(v) ? v.join(';') : String(v);
return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
};
const lines = [cols.join(',')];
for (const u of items) {
lines.push([u.username,u.password,u.vlan,u.disabled ? 'true':'false',u.etiquetas||[],u.created_at||'',u.updated_at||'',u.habilitado_since||''].map(esc).join(','));
}
res.setHeader('Content-Type','text/csv; charset=utf-8');
res.setHeader('Content-Disposition','attachment; filename="users.csv"');
res.send(lines.join('\n'));
});
router.get('/devices.csv', async (_req, res) => {
const r = await pool.query('SELECT id,mac,nombre,descripcion,vendor,first_seen,last_seen FROM dispositivos ORDER BY id ASC');
const cols = ['id','mac','nombre','descripcion','vendor','first_seen','last_seen'];
const esc = (v) => {
const s = v == null ? '' : String(v);
return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
};
const lines = [cols.join(',')];
for (const d of r.rows) lines.push(cols.map(c => esc(d[c])).join(','));
res.setHeader('Content-Type','text/csv; charset=utf-8');
res.setHeader('Content-Disposition','attachment; filename="devices.csv"');
res.send(lines.join('\n'));
});
router.get('/vlans.csv', async (_req, res) => {
const r = await pool.query('SELECT id,nombre,descripcion FROM vlans ORDER BY id ASC');
const cols = ['id','nombre','descripcion'];
const esc = (v) => {
const s = v == null ? '' : String(v);
return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
};
const lines = [cols.join(',')];
for (const d of r.rows) lines.push(cols.map(c => esc(d[c])).join(','));
res.setHeader('Content-Type','text/csv; charset=utf-8');
res.setHeader('Content-Disposition','attachment; filename="vlans.csv"');
res.send(lines.join('\n'));
});
// Import CSV: expect JSON { csv: string }
function parseCsv(str){
const rows=[]; let i=0, cur='', row=[], inq=false; for(; i<str.length; i++){ const ch=str[i]; if(ch==='"'){ if(inq && str[i+1]==='"'){ cur+='"'; i++; } else { inq=!inq; } } else if(ch===',' && !inq){ row.push(cur); cur=''; } else if((ch==='\n' || ch==='\r') && !inq){ if(ch==='\r' && str[i+1]==='\n') i++; row.push(cur); rows.push(row); row=[]; cur=''; } else { cur+=ch; } } if(cur.length||row.length) { row.push(cur); rows.push(row); } return rows; }
router.post('/users/import', async (req, res) => {
try {
const csv = String((req.body && req.body.csv) || '');
const rows = parseCsv(csv).filter(r => r.length>1);
if (rows.length === 0) return res.json({ ok: true, count: 0 });
const header = rows[0].map(s=>s.trim().toLowerCase());
const idx = (k) => header.indexOf(k);
let count=0;
for (let r of rows.slice(1)) {
const user = {
username: r[idx('username')]?.trim(),
password: r[idx('password')] ?? '',
vlan: r[idx('vlan')] ?? '',
disabled: /^(true|1|yes)$/i.test(r[idx('disabled')] || ''),
};
const tags = r[idx('etiquetas')] || '';
if (tags) user.etiquetas = String(tags).split(/[;,|]/).map(s=>s.trim()).filter(Boolean).slice(0,100);
if (user.username && user.password) { await upsertUserToDb(user); count++; }
}
res.json({ ok: true, count });
} catch (e) {
console.error('POST /api/users/import error:', e?.message || e);
res.status(500).json({ ok: false, error: 'import_error' });
}
});
router.post('/devices/import', async (req, res) => {
try {
const csv = String((req.body && req.body.csv) || '');
const rows = parseCsv(csv).filter(r => r.length>1);
if (rows.length === 0) return res.json({ ok: true, count: 0 });
const header = rows[0].map(s=>s.trim().toLowerCase());
const idx = (k) => header.indexOf(k);
let count=0;
for (let r of rows.slice(1)) {
const mac = r[idx('mac')]?.trim();
if (!mac) continue;
const nombre = r[idx('nombre')] || null;
const descripcion = r[idx('descripcion')] || null;
const vendor = r[idx('vendor')] || null;
await pool.query(
`INSERT INTO dispositivos (mac,nombre,descripcion,vendor) VALUES ($1,$2,$3,$4)
ON CONFLICT (mac) DO UPDATE SET nombre=EXCLUDED.nombre, descripcion=EXCLUDED.descripcion, vendor=EXCLUDED.vendor, last_seen=NOW()`,
[mac, nombre, descripcion, vendor]
);
count++;
}
res.json({ ok: true, count });
} catch (e) {
console.error('POST /api/devices/import error:', e?.message || e);
res.status(500).json({ ok: false, error: 'import_error' });
}
});
router.post('/vlans/import', async (req, res) => {
try {
const csv = String((req.body && req.body.csv) || '');
const rows = parseCsv(csv).filter(r => r.length>1);
if (rows.length === 0) return res.json({ ok: true, count: 0 });
const header = rows[0].map(s=>s.trim().toLowerCase());
const idx = (k) => header.indexOf(k);
let count=0;
for (let r of rows.slice(1)) {
const id = parseInt(r[idx('id')]||'',10);
if (!Number.isInteger(id)) continue;
const nombre = r[idx('nombre')] || null;
const descripcion = r[idx('descripcion')] || null;
await pool.query(
`INSERT INTO vlans (id,nombre,descripcion) VALUES ($1,$2,$3)
ON CONFLICT (id) DO UPDATE SET nombre=EXCLUDED.nombre, descripcion=EXCLUDED.descripcion`,
[id, nombre, descripcion]
);
count++;
}
res.json({ ok: true, count });
} catch (e) {
console.error('POST /api/vlans/import error:', e?.message || e);
res.status(500).json({ ok: false, error: 'import_error' });
}
});
// SSE events
router.get('/events', (req, res) => {
registerSse(req, res, {});