codigo refactorizado y ordenado, listo para siguiente fase
This commit is contained in:
149
frontend/src/App.vue
Normal file
149
frontend/src/App.vue
Normal file
@@ -0,0 +1,149 @@
|
||||
<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>
|
||||
5
frontend/src/main.js
Normal file
5
frontend/src/main.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
|
||||
createApp(App).mount('#app');
|
||||
|
||||
Reference in New Issue
Block a user