650 lines
32 KiB
HTML
650 lines
32 KiB
HTML
<div x-data="shield()" x-init="init()" class="p-6">
|
|
|
|
<!-- Header -->
|
|
<div class="flex items-center justify-between mb-6">
|
|
<div>
|
|
<h1 class="text-xl font-bold text-slate-800 flex items-center gap-2">
|
|
🛡️ USITE Shield
|
|
<span x-show="health.status === 'ok'"
|
|
class="inline-flex items-center gap-1 text-xs font-medium px-2 py-0.5 rounded-full bg-emerald-100 text-emerald-700">
|
|
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse"></span> Online
|
|
</span>
|
|
<span x-show="health.status && health.status !== 'ok'"
|
|
class="inline-flex items-center gap-1 text-xs font-medium px-2 py-0.5 rounded-full bg-red-100 text-red-700">
|
|
<span class="w-1.5 h-1.5 rounded-full bg-red-500"></span>
|
|
<span x-text="health.status || 'Offline'"></span>
|
|
</span>
|
|
</h1>
|
|
<p class="text-sm text-slate-500 mt-0.5">Panel de administración de la API de seguridad</p>
|
|
</div>
|
|
<button @click="tab='config'"
|
|
class="flex items-center gap-1.5 text-sm text-slate-500 hover:text-slate-800 px-3 py-2 rounded-lg hover:bg-slate-100 transition-colors">
|
|
⚙️ Configurar
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Tabs -->
|
|
<div class="flex gap-1 mb-6 bg-slate-100 p-1 rounded-xl w-fit flex-wrap">
|
|
<template x-for="t in tabs" :key="t.id">
|
|
<button @click="tab=t.id; loadTab(t.id)"
|
|
class="px-3 py-1.5 rounded-lg text-sm font-medium transition-colors"
|
|
:class="tab===t.id ? 'bg-white text-slate-800 shadow-sm' : 'text-slate-500 hover:text-slate-700'"
|
|
x-text="t.label">
|
|
</button>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- ═══ TAB: Estado ═══════════════════════════════════════════════════════ -->
|
|
<div x-show="tab==='estado'">
|
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
|
|
<!-- Health -->
|
|
<div class="bg-white border border-slate-200 rounded-xl p-4">
|
|
<p class="text-xs text-slate-500 font-medium uppercase tracking-wider mb-1">Estado del servicio</p>
|
|
<p class="text-lg font-bold" :class="health.status==='ok' ? 'text-emerald-600' : 'text-red-600'"
|
|
x-text="health.status==='ok' ? '🟢 Online' : (health.status || '🔴 Offline')"></p>
|
|
<p class="text-xs text-slate-400 mt-1" x-text="'DB: ' + (health.db || '-')"></p>
|
|
</div>
|
|
<!-- Extension version -->
|
|
<div class="bg-white border border-slate-200 rounded-xl p-4">
|
|
<p class="text-xs text-slate-500 font-medium uppercase tracking-wider mb-1">Versión extensión</p>
|
|
<p class="text-lg font-bold text-slate-800" x-text="extVersion.version || '—'"></p>
|
|
<a :href="extVersion.download_url" target="_blank" x-show="extVersion.download_url"
|
|
class="text-xs text-blue-500 hover:underline">Descargar</a>
|
|
</div>
|
|
<!-- Top endpoint -->
|
|
<div class="bg-white border border-slate-200 rounded-xl p-4">
|
|
<p class="text-xs text-slate-500 font-medium uppercase tracking-wider mb-1">Endpoint más activo</p>
|
|
<p class="text-sm font-bold text-slate-800 truncate" x-text="topEndpoint.path || '—'"></p>
|
|
<p class="text-xs text-slate-400 mt-1" x-text="topEndpoint.count ? topEndpoint.count + ' llamadas' : ''"></p>
|
|
</div>
|
|
</div>
|
|
<!-- Stats table -->
|
|
<div class="bg-white border border-slate-200 rounded-xl overflow-hidden">
|
|
<div class="px-4 py-3 border-b border-slate-100 flex items-center justify-between">
|
|
<span class="font-semibold text-slate-800 text-sm">Estadísticas de uso por endpoint</span>
|
|
<button @click="loadStats()" class="text-xs text-slate-400 hover:text-slate-600">↻ Actualizar</button>
|
|
</div>
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full text-sm">
|
|
<thead>
|
|
<tr class="bg-slate-50 border-b border-slate-100">
|
|
<th class="px-4 py-2 text-left text-xs text-slate-500 font-semibold">Endpoint</th>
|
|
<th class="px-4 py-2 text-left text-xs text-slate-500 font-semibold">Método</th>
|
|
<th class="px-4 py-2 text-right text-xs text-slate-500 font-semibold">Llamadas</th>
|
|
<th class="px-4 py-2 text-right text-xs text-slate-500 font-semibold">ms prom.</th>
|
|
<th class="px-4 py-2 text-right text-xs text-slate-500 font-semibold">Errores</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-slate-100">
|
|
<template x-if="stats.length === 0">
|
|
<tr><td colspan="5" class="px-4 py-6 text-center text-slate-400 text-sm">Sin datos</td></tr>
|
|
</template>
|
|
<template x-for="s in stats" :key="s.path+s.method">
|
|
<tr class="hover:bg-slate-50">
|
|
<td class="px-4 py-2 font-mono text-xs text-slate-700" x-text="s.path"></td>
|
|
<td class="px-4 py-2">
|
|
<span class="px-1.5 py-0.5 rounded text-xs font-bold"
|
|
:class="s.method==='GET'?'bg-blue-50 text-blue-700':s.method==='POST'?'bg-green-50 text-green-700':'bg-orange-50 text-orange-700'"
|
|
x-text="s.method"></span>
|
|
</td>
|
|
<td class="px-4 py-2 text-right font-medium" x-text="s.count"></td>
|
|
<td class="px-4 py-2 text-right text-slate-500" x-text="s.avg_ms + ' ms'"></td>
|
|
<td class="px-4 py-2 text-right" :class="s.errors>0?'text-red-600 font-bold':'text-slate-400'" x-text="s.errors"></td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══ TAB: Solicitudes ══════════════════════════════════════════════════ -->
|
|
<div x-show="tab==='solicitudes'">
|
|
<div class="flex items-center gap-3 mb-4">
|
|
<select x-model="reqStatus" @change="loadReviewRequests()"
|
|
class="input-field text-sm w-40">
|
|
<option value="pending">⏳ Pendientes</option>
|
|
<option value="approved">✅ Aprobadas</option>
|
|
<option value="rejected">❌ Rechazadas</option>
|
|
<option value="all">Todas</option>
|
|
</select>
|
|
<span class="text-sm text-slate-500" x-text="reviewRequests.length + ' solicitudes'"></span>
|
|
</div>
|
|
<div class="space-y-3">
|
|
<template x-if="reviewRequests.length === 0">
|
|
<div class="text-center py-10 text-slate-400">No hay solicitudes con este filtro.</div>
|
|
</template>
|
|
<template x-for="r in reviewRequests" :key="r.id">
|
|
<div class="bg-white border border-slate-200 rounded-xl p-4">
|
|
<div class="flex items-start justify-between gap-4">
|
|
<div class="flex-1 min-w-0">
|
|
<div class="flex items-center gap-2 flex-wrap">
|
|
<span class="font-bold text-slate-800" x-text="r.domain"></span>
|
|
<span class="px-2 py-0.5 rounded-full text-xs font-medium"
|
|
:class="r.request_type==='whitelist'?'bg-emerald-100 text-emerald-700':r.request_type==='blacklist'?'bg-red-100 text-red-700':'bg-blue-100 text-blue-700'"
|
|
x-text="r.request_type==='whitelist'?'⬜ Lista blanca':r.request_type==='blacklist'?'⬛ Lista negra':'Revisión'"></span>
|
|
<span class="px-2 py-0.5 rounded-full text-xs font-medium"
|
|
:class="r.status==='pending'?'bg-amber-100 text-amber-700':r.status==='approved'?'bg-emerald-100 text-emerald-700':'bg-red-100 text-red-700'"
|
|
x-text="r.status"></span>
|
|
</div>
|
|
<p x-show="r.contact_email" class="text-xs text-slate-500 mt-1" x-text="'📧 ' + r.contact_email"></p>
|
|
<p x-show="r.message" class="text-sm text-slate-600 mt-2" x-text="r.message"></p>
|
|
<p x-show="r.reviewer_note" class="text-xs text-slate-400 mt-1 italic" x-text="'Nota: ' + r.reviewer_note"></p>
|
|
<p class="text-xs text-slate-400 mt-2" x-text="'Enviada: ' + new Date(r.created_at).toLocaleString()"></p>
|
|
</div>
|
|
<!-- Acciones (solo si pendiente) -->
|
|
<div x-show="r.status==='pending'" class="flex flex-col gap-2 flex-shrink-0">
|
|
<div class="flex gap-2">
|
|
<input x-model="r._note" type="text" placeholder="Nota (opcional)"
|
|
class="input-field text-xs w-40">
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<button @click="approveRequest(r)"
|
|
class="px-3 py-1.5 rounded-lg text-xs font-medium text-white bg-emerald-600 hover:bg-emerald-700 transition-colors">
|
|
✅ Aprobar
|
|
</button>
|
|
<button @click="rejectRequest(r)"
|
|
class="px-3 py-1.5 rounded-lg text-xs font-medium text-white bg-red-500 hover:bg-red-600 transition-colors">
|
|
❌ Rechazar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══ TAB: Lista Blanca ═════════════════════════════════════════════════ -->
|
|
<div x-show="tab==='whitelist'">
|
|
<!-- Add form -->
|
|
<div class="bg-white border border-slate-200 rounded-xl p-4 mb-4">
|
|
<p class="text-sm font-semibold text-slate-700 mb-3">Agregar dominio a la lista blanca</p>
|
|
<div class="flex gap-2 flex-wrap">
|
|
<input x-model="wlForm.domain" type="text" placeholder="dominio.com"
|
|
class="input-field text-sm flex-1 min-w-[180px]">
|
|
<input x-model="wlForm.reason" type="text" placeholder="Razón"
|
|
class="input-field text-sm flex-1 min-w-[180px]">
|
|
<button @click="addWhitelist()" :disabled="!wlForm.domain || saving"
|
|
class="px-4 py-2 rounded-lg text-white text-sm font-medium transition-colors disabled:opacity-50"
|
|
style="background:#8eb02f">
|
|
+ Agregar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<!-- List -->
|
|
<div class="bg-white border border-slate-200 rounded-xl overflow-hidden">
|
|
<table class="w-full text-sm">
|
|
<thead>
|
|
<tr class="bg-slate-50 border-b border-slate-100">
|
|
<th class="px-4 py-2 text-left text-xs text-slate-500 font-semibold">Dominio</th>
|
|
<th class="px-4 py-2 text-left text-xs text-slate-500 font-semibold">Razón</th>
|
|
<th class="px-4 py-2 text-left text-xs text-slate-500 font-semibold">Agregado por</th>
|
|
<th class="px-4 py-2 text-left text-xs text-slate-500 font-semibold">Fecha</th>
|
|
<th class="px-4 py-2"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-slate-100">
|
|
<template x-if="whitelist.length === 0">
|
|
<tr><td colspan="5" class="px-4 py-6 text-center text-slate-400">Lista vacía</td></tr>
|
|
</template>
|
|
<template x-for="w in whitelist" :key="w.domain">
|
|
<tr class="hover:bg-slate-50">
|
|
<td class="px-4 py-2 font-medium text-slate-800" x-text="w.domain"></td>
|
|
<td class="px-4 py-2 text-slate-500 text-xs" x-text="w.reason || '—'"></td>
|
|
<td class="px-4 py-2 text-slate-500 text-xs" x-text="w.added_by || '—'"></td>
|
|
<td class="px-4 py-2 text-slate-400 text-xs" x-text="w.created_at ? new Date(w.created_at).toLocaleDateString() : '—'"></td>
|
|
<td class="px-4 py-2 text-right">
|
|
<button @click="deleteWhitelist(w.domain)"
|
|
class="text-xs text-red-500 hover:text-red-700 font-medium">Eliminar</button>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══ TAB: Lista Negra ══════════════════════════════════════════════════ -->
|
|
<div x-show="tab==='blacklist'">
|
|
<!-- Add form -->
|
|
<div class="bg-white border border-slate-200 rounded-xl p-4 mb-4">
|
|
<p class="text-sm font-semibold text-slate-700 mb-3">Marcar dominio como malicioso</p>
|
|
<div class="flex gap-2 flex-wrap">
|
|
<input x-model="blForm.domain" type="text" placeholder="phishing-site.xyz"
|
|
class="input-field text-sm flex-1 min-w-[180px]">
|
|
<input x-model="blForm.reason" type="text" placeholder="Razón"
|
|
class="input-field text-sm flex-1 min-w-[180px]">
|
|
<button @click="addBlacklist()" :disabled="!blForm.domain || saving"
|
|
class="px-4 py-2 rounded-lg text-white text-sm font-medium bg-red-600 hover:bg-red-700 transition-colors disabled:opacity-50">
|
|
+ Agregar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<!-- List -->
|
|
<div class="bg-white border border-slate-200 rounded-xl overflow-hidden">
|
|
<table class="w-full text-sm">
|
|
<thead>
|
|
<tr class="bg-slate-50 border-b border-slate-100">
|
|
<th class="px-4 py-2 text-left text-xs text-slate-500 font-semibold">Dominio</th>
|
|
<th class="px-4 py-2 text-left text-xs text-slate-500 font-semibold">Razón</th>
|
|
<th class="px-4 py-2 text-left text-xs text-slate-500 font-semibold">Agregado por</th>
|
|
<th class="px-4 py-2 text-left text-xs text-slate-500 font-semibold">Fecha</th>
|
|
<th class="px-4 py-2"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-slate-100">
|
|
<template x-if="blacklist.length === 0">
|
|
<tr><td colspan="5" class="px-4 py-6 text-center text-slate-400">Lista vacía</td></tr>
|
|
</template>
|
|
<template x-for="b in blacklist" :key="b.domain">
|
|
<tr class="hover:bg-slate-50">
|
|
<td class="px-4 py-2 font-medium text-red-700" x-text="b.domain"></td>
|
|
<td class="px-4 py-2 text-slate-500 text-xs" x-text="b.reason || '—'"></td>
|
|
<td class="px-4 py-2 text-slate-500 text-xs" x-text="b.added_by || '—'"></td>
|
|
<td class="px-4 py-2 text-slate-400 text-xs" x-text="b.created_at ? new Date(b.created_at).toLocaleDateString() : '—'"></td>
|
|
<td class="px-4 py-2 text-right">
|
|
<button @click="deleteBlacklist(b.domain)"
|
|
class="text-xs text-red-500 hover:text-red-700 font-medium">Eliminar</button>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══ TAB: Logs ═════════════════════════════════════════════════════════ -->
|
|
<div x-show="tab==='logs'">
|
|
<div class="flex gap-2 mb-4 flex-wrap">
|
|
<input x-model="logFilters.path" type="text" placeholder="Filtrar por ruta..."
|
|
class="input-field text-sm w-48">
|
|
<select x-model="logFilters.method" class="input-field text-sm w-28">
|
|
<option value="">Método</option>
|
|
<option>GET</option><option>POST</option><option>PUT</option><option>DELETE</option>
|
|
</select>
|
|
<select x-model="logFilters.limit" class="input-field text-sm w-24">
|
|
<option value="50">50</option>
|
|
<option value="100" selected>100</option>
|
|
<option value="250">250</option>
|
|
<option value="500">500</option>
|
|
</select>
|
|
<button @click="loadLogs()" class="px-4 py-2 rounded-lg text-white text-sm font-medium transition-colors" style="background:#8eb02f">
|
|
Buscar
|
|
</button>
|
|
</div>
|
|
<div class="bg-white border border-slate-200 rounded-xl overflow-hidden">
|
|
<div class="px-4 py-2 border-b border-slate-100 text-xs text-slate-500" x-text="logs.length + ' registros'"></div>
|
|
<div class="overflow-x-auto max-h-[500px] overflow-y-auto">
|
|
<table class="w-full text-xs">
|
|
<thead class="sticky top-0 bg-slate-50">
|
|
<tr class="border-b border-slate-100">
|
|
<th class="px-3 py-2 text-left text-slate-500 font-semibold">Método</th>
|
|
<th class="px-3 py-2 text-left text-slate-500 font-semibold">Ruta</th>
|
|
<th class="px-3 py-2 text-left text-slate-500 font-semibold">Status</th>
|
|
<th class="px-3 py-2 text-left text-slate-500 font-semibold">IP</th>
|
|
<th class="px-3 py-2 text-right text-slate-500 font-semibold">ms</th>
|
|
<th class="px-3 py-2 text-left text-slate-500 font-semibold">Fecha</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-slate-100">
|
|
<template x-if="logs.length === 0">
|
|
<tr><td colspan="6" class="px-4 py-6 text-center text-slate-400">Sin logs</td></tr>
|
|
</template>
|
|
<template x-for="l in logs" :key="l.id">
|
|
<tr class="hover:bg-slate-50">
|
|
<td class="px-3 py-1.5">
|
|
<span class="px-1.5 py-0.5 rounded text-xs font-bold"
|
|
:class="l.method==='GET'?'bg-blue-50 text-blue-700':l.method==='POST'?'bg-green-50 text-green-700':'bg-orange-50 text-orange-700'"
|
|
x-text="l.method"></span>
|
|
</td>
|
|
<td class="px-3 py-1.5 font-mono text-slate-700" x-text="l.path"></td>
|
|
<td class="px-3 py-1.5" :class="l.status>=400?'text-red-600 font-bold':'text-slate-600'" x-text="l.status"></td>
|
|
<td class="px-3 py-1.5 text-slate-400" x-text="l.ip"></td>
|
|
<td class="px-3 py-1.5 text-right text-slate-500" x-text="l.latency_ms"></td>
|
|
<td class="px-3 py-1.5 text-slate-400" x-text="l.created_at ? new Date(l.created_at).toLocaleString() : ''"></td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══ TAB: Reputación ═══════════════════════════════════════════════════ -->
|
|
<div x-show="tab==='reputacion'">
|
|
<!-- Search -->
|
|
<div class="bg-white border border-slate-200 rounded-xl p-4 mb-4">
|
|
<p class="text-sm font-semibold text-slate-700 mb-3">Consultar reputación de dominio</p>
|
|
<div class="flex gap-2">
|
|
<input x-model="repDomain" type="text" placeholder="google.com"
|
|
@keydown.enter="loadReputation()"
|
|
class="input-field text-sm flex-1">
|
|
<button @click="loadReputation()" :disabled="!repDomain"
|
|
class="px-4 py-2 rounded-lg text-white text-sm font-medium transition-colors disabled:opacity-50"
|
|
style="background:#8eb02f">
|
|
Consultar
|
|
</button>
|
|
</div>
|
|
<!-- Result -->
|
|
<div x-show="repResult" class="mt-4 grid grid-cols-2 sm:grid-cols-4 gap-3">
|
|
<div class="bg-slate-50 rounded-lg p-3 text-center">
|
|
<p class="text-xs text-slate-500 mb-1">Dominio</p>
|
|
<p class="font-bold text-slate-800 text-sm" x-text="repResult?.domain"></p>
|
|
</div>
|
|
<div class="bg-emerald-50 rounded-lg p-3 text-center">
|
|
<p class="text-xs text-slate-500 mb-1">✅ Votos Real</p>
|
|
<p class="font-bold text-emerald-700 text-xl" x-text="repResult?.real ?? 0"></p>
|
|
</div>
|
|
<div class="bg-red-50 rounded-lg p-3 text-center">
|
|
<p class="text-xs text-slate-500 mb-1">🚨 Votos Falso</p>
|
|
<p class="font-bold text-red-700 text-xl" x-text="repResult?.fake ?? 0"></p>
|
|
</div>
|
|
<div class="rounded-lg p-3 text-center"
|
|
:class="repResult?.whitelisted ? 'bg-emerald-50' : repResult?.blacklisted ? 'bg-red-50' : 'bg-slate-50'">
|
|
<p class="text-xs text-slate-500 mb-1">Estado</p>
|
|
<p class="font-bold text-sm"
|
|
:class="repResult?.whitelisted ? 'text-emerald-700' : repResult?.blacklisted ? 'text-red-700' : 'text-slate-500'"
|
|
x-text="repResult?.whitelisted ? '✅ Whitelist' : repResult?.blacklisted ? '🚫 Blacklist' : 'Neutral'"></p>
|
|
</div>
|
|
</div>
|
|
<!-- Adjust votes -->
|
|
<div x-show="repResult" class="mt-4 border-t border-slate-100 pt-4">
|
|
<p class="text-xs font-semibold text-slate-600 mb-2">Ajustar votos manualmente</p>
|
|
<div class="flex gap-2 flex-wrap items-center">
|
|
<label class="text-xs text-slate-500">Real:</label>
|
|
<input x-model.number="adjReal" type="number" min="0" class="input-field text-sm w-24">
|
|
<label class="text-xs text-slate-500">Falso:</label>
|
|
<input x-model.number="adjFake" type="number" min="0" class="input-field text-sm w-24">
|
|
<button @click="adjustScore()" :disabled="saving"
|
|
class="px-3 py-1.5 rounded-lg text-white text-xs font-medium bg-amber-600 hover:bg-amber-700 transition-colors disabled:opacity-50">
|
|
Actualizar votos
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick actions: add to whitelist/blacklist from here -->
|
|
<div x-show="repResult" class="flex gap-2 flex-wrap">
|
|
<button @click="quickWhitelist()"
|
|
class="px-3 py-2 rounded-lg text-sm font-medium text-white bg-emerald-600 hover:bg-emerald-700 transition-colors">
|
|
✅ Agregar a lista blanca
|
|
</button>
|
|
<button @click="quickBlacklist()"
|
|
class="px-3 py-2 rounded-lg text-sm font-medium text-white bg-red-600 hover:bg-red-700 transition-colors">
|
|
🚫 Agregar a lista negra
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══ TAB: Configuración ════════════════════════════════════════════════ -->
|
|
<div x-show="tab==='config'">
|
|
<div class="bg-white border border-slate-200 rounded-xl p-6 max-w-lg">
|
|
<p class="font-semibold text-slate-800 mb-4">Credenciales de USITE Shield</p>
|
|
<div class="space-y-4">
|
|
<div>
|
|
<label class="block text-xs font-medium text-slate-600 mb-1">Base URL</label>
|
|
<input x-model="configForm.base_url" type="text" placeholder="https://api-shield.u-s.app"
|
|
class="input-field w-full">
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-medium text-slate-600 mb-1">Admin Token (ADMIN_TOKEN)</label>
|
|
<input x-model="configForm.admin_token" type="password" placeholder="••••••••••••"
|
|
class="input-field w-full font-mono text-sm">
|
|
<p class="text-xs text-slate-400 mt-1">Token configurado en la variable de entorno ADMIN_TOKEN del servidor Shield.</p>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-medium text-slate-600 mb-1">Nota (opcional)</label>
|
|
<input x-model="configForm.nota" type="text" class="input-field w-full">
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<input type="checkbox" x-model="configForm.activo" id="shieldActivo" class="w-4 h-4 rounded accent-[#8eb02f]">
|
|
<label for="shieldActivo" class="text-sm text-slate-700">Configuración activa</label>
|
|
</div>
|
|
<div class="flex gap-3 pt-2">
|
|
<button @click="saveConfig()" :disabled="saving"
|
|
class="px-4 py-2 rounded-lg text-white text-sm font-medium transition-colors disabled:opacity-50"
|
|
style="background:#8eb02f">
|
|
Guardar
|
|
</button>
|
|
<button @click="testConnection()" :disabled="saving"
|
|
class="px-4 py-2 rounded-lg text-slate-700 bg-slate-100 hover:bg-slate-200 text-sm font-medium transition-colors">
|
|
Probar conexión
|
|
</button>
|
|
</div>
|
|
<p x-show="configMsg" x-text="configMsg"
|
|
class="text-sm" :class="configOk ? 'text-emerald-600' : 'text-red-500'"></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Toast -->
|
|
<div x-show="toast.visible" x-transition
|
|
class="fixed bottom-6 right-6 z-50 px-4 py-3 rounded-xl shadow-lg text-white text-sm font-medium"
|
|
:class="toast.ok ? 'bg-green-600' : 'bg-red-500'"
|
|
x-text="toast.msg">
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<script>
|
|
function shield() {
|
|
return {
|
|
tab: 'estado',
|
|
tabs: [
|
|
{ id: 'estado', label: '🟢 Estado' },
|
|
{ id: 'solicitudes', label: '📋 Solicitudes' },
|
|
{ id: 'whitelist', label: '✅ Lista Blanca' },
|
|
{ id: 'blacklist', label: '❌ Lista Negra' },
|
|
{ id: 'logs', label: '📊 Logs' },
|
|
{ id: 'reputacion', label: '🔍 Reputación' },
|
|
],
|
|
// Estado
|
|
health: {},
|
|
extVersion: {},
|
|
stats: [],
|
|
topEndpoint: {},
|
|
// Solicitudes
|
|
reviewRequests: [],
|
|
reqStatus: 'all',
|
|
// Whitelist / Blacklist
|
|
whitelist: [],
|
|
blacklist: [],
|
|
wlForm: { domain: '', reason: '' },
|
|
blForm: { domain: '', reason: '' },
|
|
// Logs
|
|
logs: [],
|
|
logFilters: { path: '', method: '', limit: '100' },
|
|
// Reputación
|
|
repDomain: '',
|
|
repResult: null,
|
|
adjReal: 0,
|
|
adjFake: 0,
|
|
// Config
|
|
configForm: { base_url: 'https://api-shield.u-s.app', admin_token: '', activo: true, nota: '' },
|
|
configMsg: '',
|
|
configOk: true,
|
|
// Util
|
|
saving: false,
|
|
toast: { visible: false, ok: true, msg: '' },
|
|
|
|
async init() {
|
|
await Promise.all([this.loadHealth(), this.loadStats(), this.loadExtVersion()]);
|
|
await this.loadConfig();
|
|
},
|
|
|
|
loadTab(id) {
|
|
if (id === 'solicitudes') this.loadReviewRequests();
|
|
if (id === 'whitelist') this.loadWhitelist();
|
|
if (id === 'blacklist') this.loadBlacklist();
|
|
if (id === 'logs') this.loadLogs();
|
|
},
|
|
|
|
async loadHealth() {
|
|
try { const r = await axios.get('/app/shield/health'); this.health = r.data; } catch {}
|
|
},
|
|
async loadExtVersion() {
|
|
try { const r = await axios.get('/app/shield/extension-version'); this.extVersion = r.data; } catch {}
|
|
},
|
|
async loadStats() {
|
|
try {
|
|
const r = await axios.get('/app/shield/logs/stats');
|
|
this.stats = r.data.stats || [];
|
|
this.topEndpoint = this.stats.reduce((a, b) => (b.count > (a.count || 0) ? b : a), {});
|
|
} catch {}
|
|
},
|
|
|
|
async loadReviewRequests() {
|
|
try {
|
|
const r = await axios.get('/app/shield/review-requests?status=' + this.reqStatus);
|
|
this.reviewRequests = (r.data.requests || []).map(x => ({ ...x, _note: '' }));
|
|
} catch (e) { this.showToast(e.response?.data?.error || 'Error al cargar', false); }
|
|
},
|
|
async approveRequest(req) {
|
|
this.saving = true;
|
|
try {
|
|
await axios.put('/app/shield/review-requests/' + req.id + '/approve', { note: req._note, reason: req._note || 'Aprobado por admin USITE' });
|
|
this.showToast('Solicitud aprobada', true);
|
|
this.loadReviewRequests();
|
|
} catch (e) { this.showToast(e.response?.data?.error || 'Error', false); }
|
|
this.saving = false;
|
|
},
|
|
async rejectRequest(req) {
|
|
this.saving = true;
|
|
try {
|
|
await axios.put('/app/shield/review-requests/' + req.id + '/reject', { note: req._note });
|
|
this.showToast('Solicitud rechazada', true);
|
|
this.loadReviewRequests();
|
|
} catch (e) { this.showToast(e.response?.data?.error || 'Error', false); }
|
|
this.saving = false;
|
|
},
|
|
|
|
async loadWhitelist() {
|
|
try { const r = await axios.get('/app/shield/whitelist'); this.whitelist = r.data.whitelist || []; }
|
|
catch (e) { this.showToast(e.response?.data?.error || 'Error', false); }
|
|
},
|
|
async addWhitelist() {
|
|
this.saving = true;
|
|
try {
|
|
await axios.post('/app/shield/whitelist', this.wlForm);
|
|
this.wlForm = { domain: '', reason: '' };
|
|
this.showToast('Dominio agregado a lista blanca', true);
|
|
this.loadWhitelist();
|
|
} catch (e) { this.showToast(e.response?.data?.error || 'Error', false); }
|
|
this.saving = false;
|
|
},
|
|
async deleteWhitelist(domain) {
|
|
if (!confirm('¿Eliminar ' + domain + ' de la lista blanca?')) return;
|
|
try {
|
|
await axios.delete('/app/shield/whitelist/' + domain);
|
|
this.showToast('Eliminado de lista blanca', true);
|
|
this.loadWhitelist();
|
|
} catch (e) { this.showToast(e.response?.data?.error || 'Error', false); }
|
|
},
|
|
|
|
async loadBlacklist() {
|
|
try { const r = await axios.get('/app/shield/blacklist'); this.blacklist = r.data.blacklist || []; }
|
|
catch (e) { this.showToast(e.response?.data?.error || 'Error', false); }
|
|
},
|
|
async addBlacklist() {
|
|
this.saving = true;
|
|
try {
|
|
await axios.post('/app/shield/blacklist', this.blForm);
|
|
this.blForm = { domain: '', reason: '' };
|
|
this.showToast('Dominio agregado a lista negra', true);
|
|
this.loadBlacklist();
|
|
} catch (e) { this.showToast(e.response?.data?.error || 'Error', false); }
|
|
this.saving = false;
|
|
},
|
|
async deleteBlacklist(domain) {
|
|
if (!confirm('¿Eliminar ' + domain + ' de la lista negra?')) return;
|
|
try {
|
|
await axios.delete('/app/shield/blacklist/' + domain);
|
|
this.showToast('Eliminado de lista negra', true);
|
|
this.loadBlacklist();
|
|
} catch (e) { this.showToast(e.response?.data?.error || 'Error', false); }
|
|
},
|
|
|
|
async loadLogs() {
|
|
try {
|
|
const params = new URLSearchParams({ limit: this.logFilters.limit });
|
|
if (this.logFilters.path) params.append('path', this.logFilters.path);
|
|
if (this.logFilters.method) params.append('method', this.logFilters.method);
|
|
const r = await axios.get('/app/shield/logs?' + params.toString());
|
|
this.logs = r.data.logs || [];
|
|
} catch (e) { this.showToast(e.response?.data?.error || 'Error', false); }
|
|
},
|
|
|
|
async loadReputation() {
|
|
if (!this.repDomain) return;
|
|
try {
|
|
const r = await axios.get('/app/shield/reputation/' + encodeURIComponent(this.repDomain));
|
|
this.repResult = r.data;
|
|
this.adjReal = r.data.real || 0;
|
|
this.adjFake = r.data.fake || 0;
|
|
} catch (e) { this.showToast(e.response?.data?.error || 'Error', false); }
|
|
},
|
|
async adjustScore() {
|
|
if (!this.repResult) return;
|
|
this.saving = true;
|
|
try {
|
|
await axios.put('/app/shield/reputation/' + encodeURIComponent(this.repDomain) + '/score', { votes_real: this.adjReal, votes_fake: this.adjFake });
|
|
this.showToast('Votos actualizados', true);
|
|
this.loadReputation();
|
|
} catch (e) { this.showToast(e.response?.data?.error || 'Error', false); }
|
|
this.saving = false;
|
|
},
|
|
async quickWhitelist() {
|
|
if (!this.repDomain) return;
|
|
try {
|
|
await axios.post('/app/shield/whitelist', { domain: this.repDomain, reason: 'Agregado desde consulta de reputación', added_by: 'admin' });
|
|
this.showToast(this.repDomain + ' agregado a lista blanca', true);
|
|
this.loadReputation();
|
|
} catch (e) { this.showToast(e.response?.data?.error || 'Error', false); }
|
|
},
|
|
async quickBlacklist() {
|
|
if (!this.repDomain) return;
|
|
try {
|
|
await axios.post('/app/shield/blacklist', { domain: this.repDomain, reason: 'Marcado desde consulta de reputación', added_by: 'admin' });
|
|
this.showToast(this.repDomain + ' agregado a lista negra', true);
|
|
this.loadReputation();
|
|
} catch (e) { this.showToast(e.response?.data?.error || 'Error', false); }
|
|
},
|
|
|
|
async loadConfig() {
|
|
try { const r = await axios.get('/app/loadshield'); this.configForm = r.data; } catch {}
|
|
},
|
|
async saveConfig() {
|
|
this.saving = true;
|
|
try {
|
|
await axios.post('/app/saveshield', this.configForm);
|
|
this.configMsg = '✅ Guardado correctamente';
|
|
this.configOk = true;
|
|
} catch (e) {
|
|
this.configMsg = '❌ ' + (e.response?.data?.error || 'Error al guardar');
|
|
this.configOk = false;
|
|
}
|
|
this.saving = false;
|
|
setTimeout(() => this.configMsg = '', 3000);
|
|
},
|
|
async testConnection() {
|
|
this.saving = true;
|
|
try {
|
|
const r = await axios.get('/app/shield/health');
|
|
this.configMsg = r.data.status === 'ok' ? '✅ Conexión OK — DB: ' + r.data.db : '⚠️ Servicio responde pero estado: ' + r.data.status;
|
|
this.configOk = r.data.status === 'ok';
|
|
} catch (e) {
|
|
this.configMsg = '❌ Sin conexión: ' + (e.response?.data?.error || e.message);
|
|
this.configOk = false;
|
|
}
|
|
this.saving = false;
|
|
},
|
|
|
|
showToast(msg, ok) {
|
|
this.toast = { visible: true, ok, msg };
|
|
setTimeout(() => this.toast.visible = false, 3500);
|
|
},
|
|
};
|
|
}
|
|
</script>
|