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(); } }