150 lines
5.3 KiB
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>
|