Files
soft_usite/resources/views/shield.html
T
2026-05-15 23:45:44 -05:00

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>