agregado de nuevos usuarios listo

This commit is contained in:
2025-09-24 17:51:05 -06:00
parent 85886f1fed
commit 11b95c97a7
11 changed files with 275 additions and 42 deletions

View File

@@ -1,7 +1,9 @@
import express from 'express';
import morgan from 'morgan';
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
import http from 'http';
import dgram from 'dgram';
import radius from 'radius';
@@ -21,10 +23,18 @@ const MAX_REQUESTS = parseInt(process.env.MAX_REQUESTS || '200', 10);
const RADIUS_HOST = process.env.RADIUS_HOST || 'freeradius';
const RADIUS_AUTH_PORT = parseInt(process.env.RADIUS_AUTH_PORT || '1812', 10);
const RADIUS_SECRET = process.env.RADIUS_SECRET || process.env.RADIUS_SHARED_SECRET || 'tamosbien';
const DOCKER_SOCK = process.env.DOCKER_SOCK || '/var/run/docker.sock';
const FREERADIUS_CONTAINER = process.env.FREERADIUS_CONTAINER || 'radiusnucleo-freeradius-1';
// In-memory request store + SSE clients
const requests = [];
// In-memory users: username -> { password, vlan }
const users = new Map([
['user1', { password: 'contra1', vlan: '2' }],
['user2', { password: 'contra2', vlan: '3' }],
]);
const sseClients = new Set();
let radiusReloading = false;
function pushRequest(rec) {
requests.push(rec);
@@ -36,6 +46,60 @@ function pushRequest(rec) {
}
}
function broadcastStatus(payload) {
const ev = `event: status\n` + `data: ${JSON.stringify(payload)}\n\n`;
for (const res of sseClients) { try { res.write(ev); } catch {}
}
}
const AUTH_FILE = process.env.AUTH_FILE || '/shared/authorize';
async function persistUsersToFreeradius() {
try {
const header = '# Managed by Node dashboard; do not edit manually\n';
const blocks = [];
for (const [username, { password, vlan }] of users.entries()) {
const v = String(vlan || VLAN_ID);
blocks.push(`${username} Cleartext-Password := "${password}"
Tunnel-Type = VLAN,
Tunnel-Medium-Type = IEEE-802,
Tunnel-Private-Group-Id = "${v}"\n`);
}
await fs.writeFile(AUTH_FILE, header + blocks.join('\n'));
// Trigger FreeRADIUS reload via Docker API (HUP)
triggerRadiusReload().catch(() => {});
} catch (e) {
console.error('Failed to persist users to FreeRADIUS files:', e);
}
}
async function triggerRadiusReload() {
try {
radiusReloading = true;
broadcastStatus({ radius_reloading: true });
// Call Docker Engine API over unix socket: POST /containers/{id}/kill?signal=HUP
await new Promise((resolve, reject) => {
const req = http.request({
method: 'POST',
socketPath: DOCKER_SOCK,
path: `/v1.41/containers/${encodeURIComponent(FREERADIUS_CONTAINER)}/kill?signal=HUP`,
}, (res) => {
res.resume();
res.on('end', resolve);
});
req.on('error', reject);
req.end();
});
} catch (e) {
console.error('Failed to HUP FreeRADIUS:', e?.message || e);
} finally {
setTimeout(() => {
radiusReloading = false;
broadcastStatus({ radius_reloading: false });
}, 1500);
}
}
// Helper: standard Accept with VLAN + bandwidth
function buildAcceptPayload(extra = {}) {
return {
@@ -105,12 +169,9 @@ app.post('/authorize-inner', (req, res) => {
console.log(JSON.stringify(req.body, null, 2));
const attrs = normalizeAttributes(req.body);
const users = {
'user1': 'contra1',
'user2': 'contra2',
};
const username = (attrs['User-Name'] || '').toString();
const password = users[username];
const entry = users.get(username);
const password = entry?.password;
if (!password) {
pushRequest({
@@ -133,12 +194,43 @@ app.post('/authorize-inner', (req, res) => {
});
return res.status(200).json({
control: {
'Cleartext-Password': password,
},
control: [
{ name: 'Cleartext-Password', value: String(password) }
]
});
});
// Post-auth: return reply attributes like VLAN based on user mapping
app.post('/post-auth', (req, res) => {
const attrs = normalizeAttributes(req.body);
const username = (attrs['User-Name'] || '').toString();
const vlan = users.get(username)?.vlan || VLAN_ID;
return res.status(200).json({
reply: [
{ name: 'Tunnel-Type', value: 'VLAN' },
{ name: 'Tunnel-Medium-Type', value: 'IEEE-802' },
{ name: 'Tunnel-Private-Group-Id', value: String(vlan) },
{ name: 'WISPr-Bandwidth-Max-Down', value: String(MAX_DOWN) },
{ name: 'WISPr-Bandwidth-Max-Up', value: String(MAX_UP) }
]
});
});
// Users API
app.get('/api/users', (req, res) => {
const items = Array.from(users.entries()).map(([username, { password, vlan }]) => ({ username, password, vlan }));
res.json({ items });
});
app.post('/api/users', (req, res) => {
const { username, password, vlan } = req.body || {};
if (!username || !password) return res.status(400).json({ ok: false, error: 'username and password required' });
const vlanStr = vlan ? String(vlan) : VLAN_ID;
users.set(String(username), { password: String(password), vlan: vlanStr });
persistUsersToFreeradius().then(() => console.log('Users synced to FreeRADIUS files'));
res.json({ ok: true });
});
// API: recent requests
app.get('/api/requests', (req, res) => {
res.json({ items: requests.slice(-MAX_REQUESTS) });
@@ -196,6 +288,9 @@ app.get('/events', (req, res) => {
// send a hello event
res.write(`event: hello\n`);
res.write(`data: {"ok":true}\n\n`);
// send initial status
res.write(`event: status\n`);
res.write(`data: ${JSON.stringify({ radius_reloading: radiusReloading })}\n\n`);
sseClients.add(res);
req.on('close', () => sseClients.delete(res));
});

View File

@@ -29,6 +29,7 @@
<span class="chip" id="status">Conectando…</span>
<span class="chip" id="count">0 eventos</span>
<span class="chip" id="band">VLAN 2 • 10/10 Mbps</span>
<span class="chip" id="radiusState" style="background:#fee; color:#900; display:none;">RADIUS: Recargando…</span>
</div>
<div class="toolbar">
<button id="refresh">Recargar historial</button>
@@ -38,8 +39,23 @@
<button id="clear">Limpiar</button>
<button id="copy">Copiar</button>
<button id="exportCsv">Exportar CSV</button>
<button id="addUser">Añadir usuario</button>
</div>
<div id="list" class="list"></div>
<div id="modal" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.4); align-items:center; justify-content:center;">
<div style="background:#fff; color:#222; padding:16px; border-radius:8px; min-width:300px; max-width:90%;">
<h3 style="margin-top:0;">Nuevo usuario</h3>
<div style="display:flex; flex-direction:column; gap:8px;">
<label>Usuario <input id="u_username" /></label>
<label>Contraseña <input id="u_password" type="password" /></label>
<label>VLAN <input id="u_vlan" type="number" min="1" value="2" /></label>
</div>
<div style="display:flex; gap:8px; margin-top:12px;">
<button id="saveUser">Guardar</button>
<button id="cancelUser">Cancelar</button>
</div>
</div>
</div>
</main>
<script>
@@ -50,10 +66,18 @@ const btnRefresh = document.getElementById('refresh');
const btnSelfTest = document.getElementById('selfTest');
const selfTestStatus = document.getElementById('selfTestStatus');
const autoScroll = document.getElementById('autoScroll');
const radiusState = document.getElementById('radiusState');
let total = 0;
const btnClear = document.getElementById('clear');
const btnCopy = document.getElementById('copy');
const btnExportCsv = document.getElementById('exportCsv');
const btnAddUser = document.getElementById('addUser');
const modal = document.getElementById('modal');
const u_username = document.getElementById('u_username');
const u_password = document.getElementById('u_password');
const u_vlan = document.getElementById('u_vlan');
const saveUser = document.getElementById('saveUser');
const cancelUser = document.getElementById('cancelUser');
let history = [];
function renderItem(ev) {
@@ -142,6 +166,41 @@ btnExportCsv.addEventListener('click', () => {
a.remove();
});
btnAddUser.addEventListener('click', () => {
modal.style.display = 'flex';
u_username.value = '';
u_password.value = '';
u_vlan.value = '2';
u_username.focus();
});
cancelUser.addEventListener('click', () => {
modal.style.display = 'none';
});
saveUser.addEventListener('click', async () => {
const username = u_username.value.trim();
const password = u_password.value;
const vlan = u_vlan.value || '2';
if (!username || !password) {
alert('Usuario y contraseña son obligatorios');
return;
}
try {
const r = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password, vlan })
});
const data = await r.json();
if (!data.ok) throw new Error(data.error || 'Error desconocido');
modal.style.display = 'none';
alert('Usuario guardado');
} catch (e) {
alert('Error al guardar: ' + e.message);
}
});
// SSE live events
function connectSSE() {
const es = new EventSource('/events');
@@ -150,6 +209,12 @@ function connectSSE() {
es.addEventListener('message', (e) => {
try { const ev = JSON.parse(e.data); renderItem(ev); } catch {}
});
es.addEventListener('status', (e) => {
try {
const s = JSON.parse(e.data);
radiusState.style.display = s.radius_reloading ? 'inline-block' : 'none';
} catch {}
});
es.addEventListener('clear', () => { loadHistory(); });
}