agregado de nuevos usuarios listo
This commit is contained in:
@@ -12,6 +12,11 @@ services:
|
|||||||
- RADIUS_HOST=freeradius
|
- RADIUS_HOST=freeradius
|
||||||
- RADIUS_AUTH_PORT=1812
|
- RADIUS_AUTH_PORT=1812
|
||||||
- RADIUS_SECRET=${RADIUS_SHARED_SECRET:-testing123}
|
- RADIUS_SECRET=${RADIUS_SHARED_SECRET:-testing123}
|
||||||
|
volumes:
|
||||||
|
- ./node-api/index.js:/app/index.js:ro
|
||||||
|
- ./node-api/public:/app/public:ro
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- ./freeradius/mods-config/files/authorize:/shared/authorize
|
||||||
networks:
|
networks:
|
||||||
- radius_net
|
- radius_net
|
||||||
|
|
||||||
@@ -29,8 +34,11 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./freeradius/mods-available/rest:/etc/freeradius/mods-available/rest:ro
|
- ./freeradius/mods-available/rest:/etc/freeradius/mods-available/rest:ro
|
||||||
- ./freeradius/mods-available/rest:/etc/freeradius/mods-enabled/rest:ro
|
- ./freeradius/mods-available/rest:/etc/freeradius/mods-enabled/rest:ro
|
||||||
|
- ./freeradius/mods-available/rest_inner:/etc/freeradius/mods-available/rest_inner:ro
|
||||||
|
- ./freeradius/mods-available/rest_inner:/etc/freeradius/mods-enabled/rest_inner:ro
|
||||||
|
- ./freeradius/mods-enabled/eap:/etc/freeradius/mods-enabled/eap:ro
|
||||||
- ./freeradius/sites-enabled/default:/etc/freeradius/sites-enabled/default:ro
|
- ./freeradius/sites-enabled/default:/etc/freeradius/sites-enabled/default:ro
|
||||||
# inner-tunnel: usar el archivo por defecto del contenedor
|
- ./freeradius/sites-enabled/inner-tunnel:/etc/freeradius/sites-enabled/inner-tunnel:ro
|
||||||
- ./freeradius/mods-config/files/authorize:/etc/freeradius/mods-config/files/authorize:ro
|
- ./freeradius/mods-config/files/authorize:/etc/freeradius/mods-config/files/authorize:ro
|
||||||
- ./freeradius/clients.conf:/etc/freeradius/clients.conf:ro
|
- ./freeradius/clients.conf:/etc/freeradius/clients.conf:ro
|
||||||
command: ["-X"]
|
command: ["-X"]
|
||||||
|
|||||||
@@ -25,4 +25,11 @@ rest {
|
|||||||
method = "post"
|
method = "post"
|
||||||
body = "json"
|
body = "json"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Post-auth: obtener atributos de respuesta (VLAN, etc.)
|
||||||
|
post-auth {
|
||||||
|
uri = "http://node:3000/post-auth"
|
||||||
|
method = "post"
|
||||||
|
body = "json"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11
freeradius/mods-available/rest_inner
Normal file
11
freeradius/mods-available/rest_inner
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
rest rest_inner {
|
||||||
|
connect_timeout = 4
|
||||||
|
read_timeout = 8
|
||||||
|
|
||||||
|
authorize {
|
||||||
|
uri = "http://node:3000/authorize-inner"
|
||||||
|
method = "post"
|
||||||
|
body = "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,3 +1,15 @@
|
|||||||
|
# Managed by Node dashboard; do not edit manually
|
||||||
user1 Cleartext-Password := "contra1"
|
user1 Cleartext-Password := "contra1"
|
||||||
user2 Cleartext-Password := "contra2"
|
Tunnel-Type = VLAN,
|
||||||
|
Tunnel-Medium-Type = IEEE-802,
|
||||||
|
Tunnel-Private-Group-Id = "2"
|
||||||
|
|
||||||
|
user2 Cleartext-Password := "contra2"
|
||||||
|
Tunnel-Type = VLAN,
|
||||||
|
Tunnel-Medium-Type = IEEE-802,
|
||||||
|
Tunnel-Private-Group-Id = "3"
|
||||||
|
|
||||||
|
prueba2 Cleartext-Password := "contra2"
|
||||||
|
Tunnel-Type = VLAN,
|
||||||
|
Tunnel-Medium-Type = IEEE-802,
|
||||||
|
Tunnel-Private-Group-Id = "2"
|
||||||
|
|||||||
@@ -1,2 +1,30 @@
|
|||||||
$INCLUDE /etc/freeradius/mods-available/eap
|
eap {
|
||||||
|
default_eap_type = peap
|
||||||
|
|
||||||
|
tls-config tls-common {
|
||||||
|
private_key_password = whatever
|
||||||
|
private_key_file = ${certdir}/server.pem
|
||||||
|
certificate_file = ${certdir}/server.pem
|
||||||
|
ca_file = ${cadir}/ca.pem
|
||||||
|
dh_file = ${certdir}/dh
|
||||||
|
random_file = /dev/urandom
|
||||||
|
fragment_size = 1024
|
||||||
|
include_length = yes
|
||||||
|
auto_chain = yes
|
||||||
|
}
|
||||||
|
|
||||||
|
tls {
|
||||||
|
tls = tls-common
|
||||||
|
}
|
||||||
|
|
||||||
|
peap {
|
||||||
|
tls = tls-common
|
||||||
|
default_eap_type = mschapv2
|
||||||
|
copy_request_to_tunnel = yes
|
||||||
|
use_tunneled_reply = yes
|
||||||
|
virtual_server = "inner-tunnel"
|
||||||
|
}
|
||||||
|
|
||||||
|
mschapv2 {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -40,28 +40,7 @@ server default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
post-auth {
|
post-auth {
|
||||||
# Asignación de VLAN dinámica por usuario
|
# Obtener atributos de VLAN/otros desde el API
|
||||||
if (&User-Name == "user1") {
|
rest.post-auth
|
||||||
update reply {
|
|
||||||
Tunnel-Type := VLAN
|
|
||||||
Tunnel-Medium-Type := IEEE-802
|
|
||||||
Tunnel-Private-Group-Id := "2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
elsif (&User-Name == "user2") {
|
|
||||||
update reply {
|
|
||||||
Tunnel-Type := VLAN
|
|
||||||
Tunnel-Medium-Type := IEEE-802
|
|
||||||
Tunnel-Private-Group-Id := "5"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
# Fallback opcional: comentar si no quieres valor por defecto
|
|
||||||
update reply {
|
|
||||||
Tunnel-Type := VLAN
|
|
||||||
Tunnel-Medium-Type := IEEE-802
|
|
||||||
Tunnel-Private-Group-Id := "2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,16 +6,16 @@ server inner-tunnel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
authorize {
|
authorize {
|
||||||
# Primero obtenemos credenciales del usuario desde el API
|
# Obtener credenciales del usuario desde el API (debe devolver Cleartext-Password)
|
||||||
rest.authorize_inner_tunnel
|
rest_inner
|
||||||
# Luego dejamos que EAP procese (PEAP/MSCHAPv2)
|
# Fallback/local: también consultar backend 'files' (user1/user2)
|
||||||
|
files
|
||||||
|
# Procesar EAP (PEAP) y MS-CHAPv2
|
||||||
eap
|
eap
|
||||||
# mschap puede establecer Auth-Type si procede
|
|
||||||
mschap
|
mschap
|
||||||
}
|
}
|
||||||
|
|
||||||
authenticate {
|
authenticate {
|
||||||
# Autenticación EAP (PEAP/MSCHAPv2)
|
|
||||||
eap
|
eap
|
||||||
Auth-Type MS-CHAP {
|
Auth-Type MS-CHAP {
|
||||||
mschap
|
mschap
|
||||||
@@ -23,7 +23,6 @@ server inner-tunnel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
post-auth {
|
post-auth {
|
||||||
# Aquí podríamos añadir lógica adicional de auditoría si se desea
|
# Nada aquí; el outer post-auth añadirá VLAN
|
||||||
# No agregamos atributos de reply aquí; se añadirán en el outer post-auth
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
29
freeradius/startup.sh
Executable file
29
freeradius/startup.sh
Executable file
@@ -0,0 +1,29 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
AUTH_FILE="/etc/freeradius/mods-config/files/authorize"
|
||||||
|
|
||||||
|
# Start FreeRADIUS in foreground
|
||||||
|
# Start in debug/foreground mode for logs
|
||||||
|
freeradius -X &
|
||||||
|
PID=$!
|
||||||
|
|
||||||
|
prev_mtime=""
|
||||||
|
|
||||||
|
poll_reload() {
|
||||||
|
while true; do
|
||||||
|
if [ -f "$AUTH_FILE" ]; then
|
||||||
|
mtime=$(stat -c %Y "$AUTH_FILE" 2>/dev/null || stat -f %m "$AUTH_FILE" 2>/dev/null || echo "")
|
||||||
|
if [ "${mtime}" != "${prev_mtime}" ] && [ -n "$mtime" ]; then
|
||||||
|
# File changed: send HUP to reload users
|
||||||
|
kill -HUP "$PID" 2>/dev/null || true
|
||||||
|
prev_mtime="$mtime"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
poll_reload &
|
||||||
|
|
||||||
|
wait "$PID"
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import morgan from 'morgan';
|
import morgan from 'morgan';
|
||||||
|
import fs from 'fs/promises';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
import http from 'http';
|
||||||
import dgram from 'dgram';
|
import dgram from 'dgram';
|
||||||
import radius from 'radius';
|
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_HOST = process.env.RADIUS_HOST || 'freeradius';
|
||||||
const RADIUS_AUTH_PORT = parseInt(process.env.RADIUS_AUTH_PORT || '1812', 10);
|
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 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
|
// In-memory request store + SSE clients
|
||||||
const requests = [];
|
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();
|
const sseClients = new Set();
|
||||||
|
let radiusReloading = false;
|
||||||
|
|
||||||
function pushRequest(rec) {
|
function pushRequest(rec) {
|
||||||
requests.push(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
|
// Helper: standard Accept with VLAN + bandwidth
|
||||||
function buildAcceptPayload(extra = {}) {
|
function buildAcceptPayload(extra = {}) {
|
||||||
return {
|
return {
|
||||||
@@ -105,12 +169,9 @@ app.post('/authorize-inner', (req, res) => {
|
|||||||
console.log(JSON.stringify(req.body, null, 2));
|
console.log(JSON.stringify(req.body, null, 2));
|
||||||
|
|
||||||
const attrs = normalizeAttributes(req.body);
|
const attrs = normalizeAttributes(req.body);
|
||||||
const users = {
|
|
||||||
'user1': 'contra1',
|
|
||||||
'user2': 'contra2',
|
|
||||||
};
|
|
||||||
const username = (attrs['User-Name'] || '').toString();
|
const username = (attrs['User-Name'] || '').toString();
|
||||||
const password = users[username];
|
const entry = users.get(username);
|
||||||
|
const password = entry?.password;
|
||||||
|
|
||||||
if (!password) {
|
if (!password) {
|
||||||
pushRequest({
|
pushRequest({
|
||||||
@@ -133,12 +194,43 @@ app.post('/authorize-inner', (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
control: {
|
control: [
|
||||||
'Cleartext-Password': password,
|
{ 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
|
// API: recent requests
|
||||||
app.get('/api/requests', (req, res) => {
|
app.get('/api/requests', (req, res) => {
|
||||||
res.json({ items: requests.slice(-MAX_REQUESTS) });
|
res.json({ items: requests.slice(-MAX_REQUESTS) });
|
||||||
@@ -196,6 +288,9 @@ app.get('/events', (req, res) => {
|
|||||||
// send a hello event
|
// send a hello event
|
||||||
res.write(`event: hello\n`);
|
res.write(`event: hello\n`);
|
||||||
res.write(`data: {"ok":true}\n\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);
|
sseClients.add(res);
|
||||||
req.on('close', () => sseClients.delete(res));
|
req.on('close', () => sseClients.delete(res));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
<span class="chip" id="status">Conectando…</span>
|
<span class="chip" id="status">Conectando…</span>
|
||||||
<span class="chip" id="count">0 eventos</span>
|
<span class="chip" id="count">0 eventos</span>
|
||||||
<span class="chip" id="band">VLAN 2 • 10/10 Mbps</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>
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<button id="refresh">Recargar historial</button>
|
<button id="refresh">Recargar historial</button>
|
||||||
@@ -38,8 +39,23 @@
|
|||||||
<button id="clear">Limpiar</button>
|
<button id="clear">Limpiar</button>
|
||||||
<button id="copy">Copiar</button>
|
<button id="copy">Copiar</button>
|
||||||
<button id="exportCsv">Exportar CSV</button>
|
<button id="exportCsv">Exportar CSV</button>
|
||||||
|
<button id="addUser">Añadir usuario</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="list" class="list"></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>
|
</main>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -50,10 +66,18 @@ const btnRefresh = document.getElementById('refresh');
|
|||||||
const btnSelfTest = document.getElementById('selfTest');
|
const btnSelfTest = document.getElementById('selfTest');
|
||||||
const selfTestStatus = document.getElementById('selfTestStatus');
|
const selfTestStatus = document.getElementById('selfTestStatus');
|
||||||
const autoScroll = document.getElementById('autoScroll');
|
const autoScroll = document.getElementById('autoScroll');
|
||||||
|
const radiusState = document.getElementById('radiusState');
|
||||||
let total = 0;
|
let total = 0;
|
||||||
const btnClear = document.getElementById('clear');
|
const btnClear = document.getElementById('clear');
|
||||||
const btnCopy = document.getElementById('copy');
|
const btnCopy = document.getElementById('copy');
|
||||||
const btnExportCsv = document.getElementById('exportCsv');
|
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 = [];
|
let history = [];
|
||||||
|
|
||||||
function renderItem(ev) {
|
function renderItem(ev) {
|
||||||
@@ -142,6 +166,41 @@ btnExportCsv.addEventListener('click', () => {
|
|||||||
a.remove();
|
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
|
// SSE live events
|
||||||
function connectSSE() {
|
function connectSSE() {
|
||||||
const es = new EventSource('/events');
|
const es = new EventSource('/events');
|
||||||
@@ -150,6 +209,12 @@ function connectSSE() {
|
|||||||
es.addEventListener('message', (e) => {
|
es.addEventListener('message', (e) => {
|
||||||
try { const ev = JSON.parse(e.data); renderItem(ev); } catch {}
|
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(); });
|
es.addEventListener('clear', () => { loadHistory(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "docker compose up -d --build",
|
"dev": "docker compose up -d --build",
|
||||||
"restart": "docker compose up -d --build --force-recreate",
|
"restart": "docker compose up -d --build --force-recreate",
|
||||||
|
"hot": "docker compose up -d node",
|
||||||
"logs": "docker compose logs -f",
|
"logs": "docker compose logs -f",
|
||||||
"down": "docker compose down",
|
"down": "docker compose down",
|
||||||
"ps": "docker compose ps"
|
"ps": "docker compose ps"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user