188 lines
6.2 KiB
JavaScript
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();
|
|
}
|
|
}
|