Files
radiusNucleo/frontend/src/App.vue

150 lines
5.3 KiB
Vue

<template>
<main style="font-family: system-ui, sans-serif; padding: 16px; max-width: 980px; margin: 0 auto;">
<h1>RADIUS Dashboard</h1>
<section style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px; align-items: start;">
<div>
<h2>Usuarios</h2>
<form @submit.prevent="createUser" style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 12px;">
<input v-model="form.username" placeholder="usuario" required />
<input v-model="form.password" placeholder="contraseña" required />
<input v-model="form.vlan" placeholder="VLAN" />
<label><input type="checkbox" v-model="form.disabled" /> deshabilitado</label>
<button type="submit">Crear / Actualizar</button>
</form>
<div v-if="loading.users">Cargando usuarios</div>
<table v-else style="width: 100%; border-collapse: collapse;">
<thead>
<tr>
<th style="text-align:left">Usuario</th>
<th style="text-align:left">VLAN</th>
<th style="text-align:left">Estado</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="u in users" :key="u.username">
<td>{{ u.username }}</td>
<td>{{ u.vlan }}</td>
<td>{{ u.disabled ? 'deshabilitado' : 'activo' }}</td>
<td>
<button @click="toggleDisable(u)">{{ u.disabled ? 'Habilitar' : 'Deshabilitar' }}</button>
<button @click="removeUser(u)" style="margin-left: 6px">Eliminar</button>
</td>
</tr>
</tbody>
</table>
</div>
<div>
<h2>Eventos</h2>
<div style="margin-bottom: 8px; display:flex; gap:8px;">
<button @click="refreshRequests">Refrescar</button>
<button @click="clearRequests">Limpiar</button>
<button @click="selfTest">Self test</button>
<a :href="'/api/requests.csv'" target="_blank">Descargar CSV</a>
</div>
<div v-if="loading.requests">Cargando eventos</div>
<div v-else style="max-height: 420px; overflow: auto; border: 1px solid #ddd; padding: 8px;">
<div v-for="ev in requests" :key="ev.id" style="border-bottom: 1px dashed #ddd; padding: 6px 0;">
<div><b>{{ ev.ts }}</b> {{ ev.type }}</div>
<div v-if="ev.attrs" style="font-size: 12px; color: #444;">
<span>User: {{ ev.attrs['User-Name'] || ev.attrs['User-Name*0'] }}</span>
<span v-if="ev.attrs['NAS-IP-Address']"> NAS: {{ ev.attrs['NAS-IP-Address'] }}</span>
<span v-if="ev.attrs['Calling-Station-Id']"> STA: {{ ev.attrs['Calling-Station-Id'] }}</span>
</div>
<div v-if="ev.decision">Decision: {{ ev.decision }}</div>
<div v-if="ev.error" style="color: #a00;">Error: {{ ev.error }}</div>
</div>
</div>
</div>
</section>
</main>
</template>
<script setup>
import { onMounted, reactive, ref } from 'vue';
const users = ref([]);
const requests = ref([]);
const loading = reactive({ users: false, requests: false });
const form = reactive({ username: '', password: '', vlan: '', disabled: false });
async function fetchUsers() {
loading.users = true;
try {
const res = await fetch('/api/users');
const data = await res.json();
users.value = data.items || [];
} finally { loading.users = false; }
}
async function fetchRequests() {
loading.requests = true;
try {
const res = await fetch('/api/requests');
const data = await res.json();
requests.value = data.items || [];
} finally { loading.requests = false; }
}
async function createUser() {
const payload = { ...form };
if (!payload.vlan) delete payload.vlan;
await fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
form.username = '';
form.password = '';
form.vlan = '';
form.disabled = false;
await fetchUsers();
}
async function toggleDisable(u) {
await fetch(`/api/users/${encodeURIComponent(u.username)}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ disabled: !u.disabled })
});
await fetchUsers();
}
async function removeUser(u) {
if (!confirm(`Eliminar ${u.username}?`)) return;
await fetch(`/api/users/${encodeURIComponent(u.username)}`, { method: 'DELETE' });
await fetchUsers();
}
async function refreshRequests() { await fetchRequests(); }
async function clearRequests() {
await fetch('/api/requests', { method: 'DELETE' });
await fetchRequests();
}
async function selfTest() {
await fetch('/test/radius', { method: 'POST' });
}
function setupSse() {
const ev = new EventSource('/api/events');
ev.addEventListener('message', (e) => {
try {
const data = JSON.parse(e.data);
if (data && data.ts) requests.value.push(data);
} catch {}
});
ev.addEventListener('clear', () => { requests.value = []; });
}
onMounted(async () => {
await fetchUsers();
await fetchRequests();
setupSse();
});
</script>
<style>
html, body { margin: 0; padding: 0; }
table th, table td { padding: 4px 6px; border-bottom: 1px solid #eee; }
button { padding: 6px 10px; cursor: pointer; }
input { padding: 6px 8px; }
</style>