tablas y frontend visualizador de DB listo
This commit is contained in:
@@ -1,15 +1,20 @@
|
||||
import { Router } from 'express';
|
||||
import { VLAN_ID } from '../config/env.js';
|
||||
import { clearRequests, getRecentRequests, registerSse } from '../sse.js';
|
||||
import { deleteUserFromDb, readUsersFromDb, upsertUserToDb } from '../services/db.js';
|
||||
import { deleteUserFromDb, readUsersFromDb, upsertUserToDb, pool } from '../services/db.js';
|
||||
import { disconnectUserSessions } from '../services/radius.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Users
|
||||
router.get('/users', async (_req, res) => {
|
||||
const items = await readUsersFromDb();
|
||||
res.json({ items });
|
||||
try {
|
||||
const items = await readUsersFromDb();
|
||||
res.json({ items });
|
||||
} catch (e) {
|
||||
console.error('GET /api/users error:', e?.message || e);
|
||||
res.status(500).json({ items: [], ok: false, error: 'db_error' });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/users', async (req, res) => {
|
||||
@@ -89,4 +94,36 @@ router.get('/events', (req, res) => {
|
||||
registerSse(req, res, {});
|
||||
});
|
||||
|
||||
// Raw DB views (read-only)
|
||||
router.get('/db/tables', async (_req, res) => {
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
"SELECT table_name FROM information_schema.tables WHERE table_schema='public' AND table_type='BASE TABLE' ORDER BY table_name ASC"
|
||||
);
|
||||
res.json({ items: rows.map(r => r.table_name) });
|
||||
} catch (e) {
|
||||
res.status(500).json({ ok: false, error: 'db_error' });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/db/table/:name', async (req, res) => {
|
||||
const t = String(req.params.name);
|
||||
try {
|
||||
// validate table name exists and is public
|
||||
const { rows } = await pool.query(
|
||||
"SELECT table_name FROM information_schema.tables WHERE table_schema='public' AND table_type='BASE TABLE' AND table_name = $1",
|
||||
[t]
|
||||
);
|
||||
if (rows.length === 0) return res.status(404).json({ ok: false, error: 'not_found' });
|
||||
const limit = Math.max(1, Math.min(1000, parseInt(String(req.query.limit || '100'), 10) || 100));
|
||||
const offset = Math.max(0, parseInt(String(req.query.offset || '0'), 10) || 0);
|
||||
const totalRes = await pool.query(`SELECT COUNT(*)::int AS n FROM "${t}"`);
|
||||
const total = totalRes.rows[0]?.n || 0;
|
||||
const result = await pool.query(`SELECT * FROM "${t}" OFFSET $1 LIMIT $2`, [offset, limit]);
|
||||
res.json({ ok: true, columns: result.fields.map(f => f.name), rows: result.rows, total, limit, offset });
|
||||
} catch (e) {
|
||||
res.status(500).json({ ok: false, error: 'db_error' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -4,6 +4,70 @@ import { PGDATABASE, PGHOST, PGPASSWORD, PGPORT, PGUSER, SESSION_TIMEOUT, VLAN_I
|
||||
const { Pool } = pkgPg;
|
||||
export const pool = new Pool({ host: PGHOST, port: PGPORT, database: PGDATABASE, user: PGUSER, password: PGPASSWORD });
|
||||
|
||||
export async function ensureSchema() {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS vlans (
|
||||
id INTEGER PRIMARY KEY,
|
||||
nombre TEXT NOT NULL,
|
||||
descripcion TEXT
|
||||
);
|
||||
`);
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS dispositivos (
|
||||
id SERIAL PRIMARY KEY,
|
||||
mac VARCHAR(32) UNIQUE NOT NULL,
|
||||
nombre TEXT,
|
||||
descripcion TEXT,
|
||||
vendor TEXT,
|
||||
first_seen TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
last_seen TIMESTAMPTZ,
|
||||
notas TEXT
|
||||
);
|
||||
`);
|
||||
await client.query(`CREATE INDEX IF NOT EXISTS dispositivos_mac_idx ON dispositivos (mac);`);
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
username VARCHAR(64) PRIMARY KEY,
|
||||
etiquetas TEXT[] NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
habilitado_since TIMESTAMPTZ,
|
||||
dispositivos_utilizados INTEGER[] NOT NULL DEFAULT '{}',
|
||||
dispositivos_conectados INTEGER[] NOT NULL DEFAULT '{}'
|
||||
);
|
||||
`);
|
||||
await client.query(`
|
||||
CREATE OR REPLACE FUNCTION set_updated_at() RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END; $$ LANGUAGE plpgsql;
|
||||
`);
|
||||
await client.query(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_trigger WHERE tgname = 'trg_users_updated'
|
||||
) THEN
|
||||
CREATE TRIGGER trg_users_updated
|
||||
BEFORE UPDATE ON users
|
||||
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||||
END IF;
|
||||
END$$;
|
||||
`);
|
||||
await client.query('COMMIT');
|
||||
} catch (e) {
|
||||
await client.query('ROLLBACK');
|
||||
console.error('ensureSchema error:', e?.message || e);
|
||||
throw e;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
export async function readUsersFromDb() {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
@@ -18,12 +82,30 @@ export async function readUsersFromDb() {
|
||||
SELECT rr.value FROM radreply rr
|
||||
WHERE rr.username = rc.username AND rr.attribute = 'Tunnel-Private-Group-Id'
|
||||
ORDER BY rr.id DESC LIMIT 1
|
||||
), $1) AS vlan
|
||||
), $1) AS vlan,
|
||||
u.etiquetas,
|
||||
u.created_at,
|
||||
u.updated_at,
|
||||
u.habilitado_since,
|
||||
u.dispositivos_utilizados,
|
||||
u.dispositivos_conectados
|
||||
FROM radcheck rc
|
||||
LEFT JOIN users u ON u.username = rc.username
|
||||
WHERE rc.attribute = 'Cleartext-Password'
|
||||
ORDER BY rc.username ASC`;
|
||||
const { rows } = await client.query(q, [String(VLAN_ID)]);
|
||||
return rows.map(r => ({ username: r.username, password: r.password, vlan: String(r.vlan), disabled: !!r.disabled }));
|
||||
return rows.map(r => ({
|
||||
username: r.username,
|
||||
password: r.password,
|
||||
vlan: String(r.vlan),
|
||||
disabled: !!r.disabled,
|
||||
etiquetas: r.etiquetas || [],
|
||||
created_at: r.created_at || null,
|
||||
updated_at: r.updated_at || null,
|
||||
habilitado_since: r.habilitado_since || null,
|
||||
dispositivos_utilizados: r.dispositivos_utilizados || [],
|
||||
dispositivos_conectados: r.dispositivos_conectados || [],
|
||||
}));
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
@@ -34,6 +116,11 @@ export async function upsertUserToDb(user) {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
// Ensure metadata row exists
|
||||
await client.query(
|
||||
'INSERT INTO users (username) VALUES ($1) ON CONFLICT (username) DO NOTHING',
|
||||
[username]
|
||||
);
|
||||
await client.query("DELETE FROM radcheck WHERE username = $1 AND attribute = 'Cleartext-Password'", [username]);
|
||||
await client.query(
|
||||
"INSERT INTO radcheck (username, attribute, op, value) VALUES ($1,'Cleartext-Password',':=',$2)",
|
||||
@@ -66,6 +153,14 @@ export async function upsertUserToDb(user) {
|
||||
[username, attr, String(val)]
|
||||
);
|
||||
}
|
||||
// Update metadata timestamps; set habilitado_since when enabled
|
||||
await client.query(
|
||||
`UPDATE users
|
||||
SET updated_at = NOW(),
|
||||
habilitado_since = CASE WHEN $2::boolean = FALSE THEN NOW() ELSE habilitado_since END
|
||||
WHERE username = $1`,
|
||||
[username, !!disabled]
|
||||
);
|
||||
await client.query('COMMIT');
|
||||
} catch (e) {
|
||||
await client.query('ROLLBACK');
|
||||
@@ -81,6 +176,7 @@ export async function deleteUserFromDb(username) {
|
||||
await client.query('BEGIN');
|
||||
await client.query('DELETE FROM radcheck WHERE username = $1', [username]);
|
||||
await client.query('DELETE FROM radreply WHERE username = $1', [username]);
|
||||
await client.query('DELETE FROM users WHERE username = $1', [username]);
|
||||
await client.query('COMMIT');
|
||||
} catch (e) {
|
||||
await client.query('ROLLBACK');
|
||||
@@ -89,4 +185,3 @@ export async function deleteUserFromDb(username) {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user