Files
radiusNucleo/node-api/src/services/db.js

188 lines
6.2 KiB
JavaScript

import pkgPg from 'pg';
import { PGDATABASE, PGHOST, PGPASSWORD, PGPORT, PGUSER, SESSION_TIMEOUT, VLAN_ID, MAX_DOWN, MAX_UP } from '../config/env.js';
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 {
const q = `
SELECT rc.username,
rc.value AS password,
EXISTS (
SELECT 1 FROM radcheck r2
WHERE r2.username = rc.username AND r2.attribute = 'Auth-Type' AND r2.value = 'Reject'
) AS disabled,
COALESCE((
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,
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,
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();
}
}
export async function upsertUserToDb(user) {
const { username, password, vlan, disabled } = 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)",
[username, password]
);
await client.query("DELETE FROM radcheck WHERE username = $1 AND attribute = 'Auth-Type'", [username]);
if (disabled) {
await client.query(
"INSERT INTO radcheck (username, attribute, op, value) VALUES ($1,'Auth-Type',':=','Reject')",
[username]
);
}
const attrs = [
['Tunnel-Type', 'VLAN'],
['Tunnel-Medium-Type', 'IEEE-802'],
['Tunnel-Private-Group-Id', String(vlan || VLAN_ID)],
['WISPr-Bandwidth-Max-Down', String(MAX_DOWN)],
['WISPr-Bandwidth-Max-Up', String(MAX_UP)],
];
if (SESSION_TIMEOUT > 0) {
attrs.push(['Session-Timeout', String(SESSION_TIMEOUT)]);
}
await client.query(
"DELETE FROM radreply WHERE username = $1 AND attribute IN ('Tunnel-Type','Tunnel-Medium-Type','Tunnel-Private-Group-Id','WISPr-Bandwidth-Max-Down','WISPr-Bandwidth-Max-Up','Session-Timeout')",
[username]
);
for (const [attr, val] of attrs) {
await client.query(
"INSERT INTO radreply (username, attribute, op, value) VALUES ($1,$2,':=',$3)",
[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');
throw e;
} finally {
client.release();
}
}
export async function deleteUserFromDb(username) {
const client = await pool.connect();
try {
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');
throw e;
} finally {
client.release();
}
}