From 0d4b0cbf67e0c4ef4092d732efc44973adfcdfc6 Mon Sep 17 00:00:00 2001 From: josedario87 Date: Fri, 26 Sep 2025 19:10:17 -0600 Subject: [PATCH] UI/UX mejorados 5 --- frontend/src/App.vue | 19 ++++++++- frontend/src/components/DispositivoCard.vue | 1 + frontend/src/components/UserCard.vue | 6 ++- frontend/src/styles.css | 15 ++++++- node-api/src/routes/api.js | 26 ++++++++++++ node-api/src/services/radius.js | 46 +++++++++++++++++++++ 6 files changed, 108 insertions(+), 5 deletions(-) diff --git a/frontend/src/App.vue b/frontend/src/App.vue index a4c6800..2c68194 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -67,13 +67,14 @@ + @toggleDisable="toggleDisable" @remove="removeUser" @edit="openEditUser" + @disconnect="disconnectUser" @edit-device="openDeviceForm" />
+ @edit="openDeviceForm" @disconnect="disconnectDevice" />
@@ -206,6 +207,20 @@ async function selfTest() { await fetch('/test/radius', { method: 'POST' }); } +async function disconnectUser(u) { + try { + await fetch(`/api/users/${encodeURIComponent(u.username)}/disconnect`, { method: 'POST' }); + await Promise.all([fetchUsers(), fetchDevices()]); + } catch {} +} + +async function disconnectDevice(d) { + try { + await fetch(`/api/devices/${encodeURIComponent(d.id)}/disconnect`, { method: 'POST' }); + await Promise.all([fetchUsers(), fetchDevices()]); + } catch {} +} + function setupSse() { const ev = new EventSource('/api/events'); let refreshTimer = null; diff --git a/frontend/src/components/DispositivoCard.vue b/frontend/src/components/DispositivoCard.vue index 914d2f3..55dc412 100644 --- a/frontend/src/components/DispositivoCard.vue +++ b/frontend/src/components/DispositivoCard.vue @@ -7,6 +7,7 @@ Conectado +
diff --git a/frontend/src/components/UserCard.vue b/frontend/src/components/UserCard.vue index 4e6ee84..e04d166 100644 --- a/frontend/src/components/UserCard.vue +++ b/frontend/src/components/UserCard.vue @@ -4,15 +4,17 @@ {{ item.username }} VLAN {{ item.vlan }} {{ item.disabled ? 'deshabilitado' : 'activo' }} + Conectado +
- +
@@ -37,4 +39,6 @@ const deviceList = computed(() => { function isConnected(id) { return Array.isArray(props.item.dispositivos_conectados) && props.item.dispositivos_conectados.includes(id); } + +const hasConnected = computed(() => Array.isArray(props.item.dispositivos_conectados) && props.item.dispositivos_conectados.length > 0); diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 5de82b8..2c1c5e6 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -86,7 +86,9 @@ a { color: inherit; } .menu button:hover { transform: none; background: rgba(255,255,255,0.06); } /* Layout */ -.shell { height: calc(100vh - 54px); display: grid; grid-template-columns: 360px 1fr; gap: 12px; padding: 12px; } +.shell { height: calc(100vh - 54px); display: grid; grid-template-columns: 360px 1fr; grid-template-areas: "sidebar main"; gap: 12px; padding: 12px; } +.shell > aside { grid-area: sidebar; } +.shell > main { grid-area: main; } <<<<<<< HEAD .panel { border: 1px solid #ffcfe4; /* light más claro */ @@ -111,9 +113,18 @@ a { color: inherit; } /* Responsive */ @media (max-width: 980px) { - .shell { grid-template-columns: 1fr; } + .shell { grid-template-columns: 1fr; grid-template-areas: + "main" + "sidebar"; } } +/* When a panel is collapsed, hide its scroll and shrink its grid track to show only header */ +.panel.collapsed .scroll { display: none; } + +/* Using :has to adapt the grid columns when a side is collapsed (modern browsers) */ +.shell:has(> aside.collapsed) { grid-template-columns: 52px 1fr; } +.shell:has(> main.collapsed) { grid-template-columns: 1fr 52px; } + /* Collapse helpers: keep headers visible when collapsed */ .panel.collapsed .scroll { display: none; } diff --git a/node-api/src/routes/api.js b/node-api/src/routes/api.js index 401b1b6..28c4cff 100644 --- a/node-api/src/routes/api.js +++ b/node-api/src/routes/api.js @@ -176,4 +176,30 @@ router.patch('/devices/:id', async (req, res) => { } }); +router.post('/users/:username/disconnect', async (req, res) => { + try { + const uname = String(req.params.username); + await disconnectUserSessions(uname); + res.json({ ok: true }); + } catch (e) { + console.error('POST /api/users/:username/disconnect error:', e?.message || e); + res.status(500).json({ ok: false, error: 'disconnect_error' }); + } +}); + +import { disconnectByMac } from '../services/radius.js'; +router.post('/devices/:id/disconnect', async (req, res) => { + try { + const id = parseInt(String(req.params.id), 10); + if (!Number.isInteger(id) || id <= 0) return res.status(400).json({ ok: false, error: 'invalid_id' }); + const r = await pool.query('SELECT mac FROM dispositivos WHERE id = $1', [id]); + if (r.rows.length === 0) return res.status(404).json({ ok: false, error: 'not_found' }); + await disconnectByMac(r.rows[0].mac); + res.json({ ok: true }); + } catch (e) { + console.error('POST /api/devices/:id/disconnect error:', e?.message || e); + res.status(500).json({ ok: false, error: 'disconnect_error' }); + } +}); + export default router; diff --git a/node-api/src/services/radius.js b/node-api/src/services/radius.js index 513cfc8..b6ebd18 100644 --- a/node-api/src/services/radius.js +++ b/node-api/src/services/radius.js @@ -125,3 +125,49 @@ export async function disconnectUserSessions(username) { } } +export async function disconnectByMac(mac) { + if (!mac) return; + const targets = []; + for (const sess of activeSessions.values()) { + if (String(sess.callingStationId || '').toLowerCase() === String(mac).toLowerCase() && sess.nasIp) { + targets.push(sess); + } + } + if (targets.length === 0) return; + for (const sess of targets) { + try { + const result = await sendDisconnectRequest({ + nasIp: sess.nasIp, + username: sess.username, + sessionId: sess.sessionId, + callingStationId: sess.callingStationId, + nasId: sess.nasId, + }); + pushRequest({ + id: Date.now() + ':' + Math.random().toString(16).slice(2), + ts: new Date().toISOString(), + type: 'coa-disconnect', + attrs: { + 'User-Name': sess.username, + 'NAS-IP-Address': sess.nasIp, + 'Acct-Session-Id': sess.sessionId, + 'Calling-Station-Id': sess.callingStationId, + 'result': result.code, + }, + }); + } catch (e) { + pushRequest({ + id: Date.now() + ':' + Math.random().toString(16).slice(2), + ts: new Date().toISOString(), + type: 'coa-disconnect', + attrs: { + 'User-Name': sess.username, + 'NAS-IP-Address': sess.nasIp, + 'Acct-Session-Id': sess.sessionId, + 'Calling-Station-Id': sess.callingStationId, + 'error': String(e?.message || e), + }, + }); + } + } +}