up
This commit is contained in:
@@ -66,6 +66,8 @@ func main() {
|
||||
// Notificaciones
|
||||
&models.NotifEventoConfig{},
|
||||
&models.SistemaNotificacion{},
|
||||
// Shield
|
||||
&models.ShieldConfig{},
|
||||
)
|
||||
// Seed automático (idempotente) de módulos del sistema
|
||||
migrations.SeedRenovaciones()
|
||||
@@ -78,6 +80,7 @@ func main() {
|
||||
migrations.SeedTelegram()
|
||||
migrations.SeedPortalClientes()
|
||||
migrations.SeedNotifDefaults()
|
||||
migrations.SeedShield()
|
||||
// Iniciar cron de vencimientos
|
||||
services.IniciarCron()
|
||||
defer services.DetenerCron()
|
||||
|
||||
@@ -860,6 +860,7 @@ func SeedNotifDefaults() {
|
||||
{Evento: "ticket_nuevo", Destinatario: "admin", CanalEmail: true, CanalTelegram: false, CanalSistema: true, Descripcion: "Admin recibe cuando el cliente abre un ticket"},
|
||||
{Evento: "ticket_respuesta_cliente", Destinatario: "admin", CanalEmail: true, CanalTelegram: false, CanalSistema: true, Descripcion: "Admin recibe cuando el cliente responde un ticket"},
|
||||
{Evento: "ticket_respuesta_admin", Destinatario: "portal_user", CanalEmail: true, CanalTelegram: false, CanalSistema: true, Descripcion: "Cliente recibe cuando el admin responde su ticket"},
|
||||
{Evento: "factura_subida", Destinatario: "portal_user", CanalEmail: true, CanalTelegram: false, CanalSistema: true, Descripcion: "Cliente recibe cuando el admin sube el PDF de una factura"},
|
||||
}
|
||||
for _, cfg := range defaults {
|
||||
var existing models.NotifEventoConfig
|
||||
@@ -873,3 +874,41 @@ func SeedNotifDefaults() {
|
||||
}
|
||||
log.Println("[SEED] Seed de notificaciones completado.")
|
||||
}
|
||||
|
||||
// SeedShield agrega el submódulo "Shield" al módulo "Integraciones". Es idempotente.
|
||||
func SeedShield() {
|
||||
db := app.Http.Database.DB
|
||||
|
||||
var modulo models.Modules
|
||||
if err := db.Where("title = ?", "Integraciones").First(&modulo).Error; err != nil {
|
||||
log.Printf("[SEED] Módulo 'Integraciones' no encontrado para SeedShield: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var sub models.Submodules
|
||||
if err := db.Where("url = ?", "/app/shield").First(&sub).Error; err != nil {
|
||||
sub = models.Submodules{
|
||||
Title: "Shield",
|
||||
Description: "Administración de la API de seguridad USITE Shield",
|
||||
Url: "/app/shield",
|
||||
ModuleId: modulo.ID,
|
||||
ModifiedAt: time.Now(),
|
||||
}
|
||||
if err := db.Create(&sub).Error; err != nil {
|
||||
log.Printf("[SEED] Error creando submódulo Shield: %v", err)
|
||||
return
|
||||
}
|
||||
log.Printf("[SEED] Submódulo 'Shield' creado (ID %d)", sub.ID)
|
||||
} else {
|
||||
log.Printf("[SEED] Submódulo 'Shield' ya existe (ID %d)", sub.ID)
|
||||
}
|
||||
|
||||
var roles []models.Roles
|
||||
if err := db.Find(&roles).Error; err != nil {
|
||||
return
|
||||
}
|
||||
for _, rol := range roles {
|
||||
db.Model(&rol).Association("Submodules").Append(&sub)
|
||||
}
|
||||
log.Println("[SEED] SeedShield completado.")
|
||||
}
|
||||
|
||||
@@ -153,3 +153,12 @@ func CheckPortalLogin(email, password string) (*PortalUser, error) {
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// GetPortalUsersByClienteID devuelve todos los portal users activos de un cliente.
|
||||
func GetPortalUsersByClienteID(clienteID uint) ([]PortalUser, error) {
|
||||
var users []PortalUser
|
||||
err := app.Http.Database.DB.
|
||||
Where("cliente_id = ? AND activo = true", clienteID).
|
||||
Find(&users).Error
|
||||
return users, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/sujit-baniya/fiber-boilerplate/app"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ShieldConfig almacena las credenciales para la API de USITE Shield.
|
||||
type ShieldConfig struct {
|
||||
gorm.Model
|
||||
BaseUrl string `json:"base_url" gorm:"column:base_url;type:text;default:'https://api-shield.u-s.app'"`
|
||||
AdminToken string `json:"admin_token" gorm:"column:admin_token;type:text"`
|
||||
Activo bool `json:"activo" gorm:"column:activo;default:true"`
|
||||
Nota string `json:"nota" gorm:"column:nota;type:text"`
|
||||
}
|
||||
|
||||
func (ShieldConfig) TableName() string { return "shield_config" }
|
||||
|
||||
func GetShieldConfig() (*ShieldConfig, error) {
|
||||
var cfg ShieldConfig
|
||||
err := app.Http.Database.DB.Where("activo = true").First(&cfg).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
func UpsertShieldConfig(cfg *ShieldConfig) error {
|
||||
var existing ShieldConfig
|
||||
if err := app.Http.Database.DB.First(&existing).Error; err != nil {
|
||||
return app.Http.Database.DB.Create(cfg).Error
|
||||
}
|
||||
return app.Http.Database.DB.Model(&existing).Updates(map[string]interface{}{
|
||||
"base_url": cfg.BaseUrl,
|
||||
"admin_token": cfg.AdminToken,
|
||||
"activo": cfg.Activo,
|
||||
"nota": cfg.Nota,
|
||||
}).Error
|
||||
}
|
||||
@@ -176,3 +176,25 @@ func SendTicketRespuestaAdmin(adminEmail, proyectoNombre, autorNombre, titulo, r
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// SendFacturaSubidaPortalUser notifica a un portal user que tiene una nueva factura disponible.
|
||||
func SendFacturaSubidaPortalUser(toEmail, userName, clienteNombre, numero string, monto float64, moneda, portalURL string) {
|
||||
go func() {
|
||||
subject := fmt.Sprintf("Nueva factura disponible: %s", numero)
|
||||
htmlBody := fmt.Sprintf(`
|
||||
<div style="font-family:sans-serif;max-width:520px;margin:0 auto;padding:24px">
|
||||
<h2 style="color:#8eb02f">🧾 Nueva factura disponible</h2>
|
||||
<p>Hola <strong>%s</strong>,</p>
|
||||
<p>Tienes una nueva factura disponible para descargar:</p>
|
||||
<table style="width:100%%;border-collapse:collapse;margin:16px 0">
|
||||
<tr><td style="padding:8px;border:1px solid #e2e8f0;background:#f8fafc;font-weight:bold">Número</td><td style="padding:8px;border:1px solid #e2e8f0">%s</td></tr>
|
||||
<tr><td style="padding:8px;border:1px solid #e2e8f0;background:#f8fafc;font-weight:bold">Monto</td><td style="padding:8px;border:1px solid #e2e8f0">%s %.2f</td></tr>
|
||||
</table>
|
||||
<a href="%s" style="display:inline-block;background:#8eb02f;color:white;padding:12px 24px;border-radius:8px;text-decoration:none;font-weight:bold">Ver en el portal</a>
|
||||
<p style="margin-top:24px;color:#94a3b8;font-size:12px">Este es un mensaje automático.</p>
|
||||
</div>`, userName, numero, moneda, monto, portalURL)
|
||||
if err := app.Http.Mail.Send(toEmail, subject, htmlBody, ""); err != nil {
|
||||
log.Printf("[Notif] Error enviando factura_subida a %s: %v", toEmail, err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/sujit-baniya/fiber-boilerplate/app"
|
||||
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
||||
)
|
||||
|
||||
@@ -16,7 +15,7 @@ func DispatchTicketNuevo(ticket *models.ProyectoTicket, portalUser *models.Porta
|
||||
if cfg == nil {
|
||||
return
|
||||
}
|
||||
ticketURL := fmt.Sprintf("%s/app/tickets", app.Http.Server.Url)
|
||||
ticketURL := "/app/tickets"
|
||||
titulo := fmt.Sprintf("Nuevo ticket: %s", ticket.Titulo)
|
||||
cuerpo := fmt.Sprintf("Cliente %s abrió: %s", ticket.AutorNombre, ticket.Titulo)
|
||||
|
||||
@@ -50,7 +49,7 @@ func DispatchTicketRespuestaCliente(ticket *models.ProyectoTicket, contenido str
|
||||
if cfg == nil {
|
||||
return
|
||||
}
|
||||
ticketURL := fmt.Sprintf("%s/app/tickets", app.Http.Server.Url)
|
||||
ticketURL := "/app/tickets"
|
||||
titulo := fmt.Sprintf("Respuesta de cliente: %s", ticket.Titulo)
|
||||
cuerpo := fmt.Sprintf("%s respondió: %s", ticket.AutorNombre, contenido)
|
||||
|
||||
@@ -91,7 +90,7 @@ func DispatchTicketRespuestaAdmin(ticket *models.ProyectoTicket, contenido strin
|
||||
return
|
||||
}
|
||||
|
||||
portalURL := fmt.Sprintf("%s/portal/proyecto/%s", app.Http.Server.Url, ticket.Proyecto.Slug)
|
||||
portalURL := fmt.Sprintf("/portal/proyecto/%s", ticket.Proyecto.Slug)
|
||||
titulo := fmt.Sprintf("Respuesta en tu ticket: %s", ticket.Titulo)
|
||||
cuerpo := fmt.Sprintf("El equipo respondió: %s", contenido)
|
||||
|
||||
@@ -160,3 +159,56 @@ func sendTelegramAdmin(mensaje string) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// ─── DispatchFacturaSubida ────────────────────────────────────────────────────
|
||||
// Llamar cuando el admin sube el PDF de una factura.
|
||||
// Notifica a todos los portal_users activos del cliente.
|
||||
func DispatchFacturaSubida(factura *models.Factura) {
|
||||
cfg := models.GetNotifConfig("factura_subida", "portal_user")
|
||||
if cfg == nil {
|
||||
return
|
||||
}
|
||||
if factura.ClienteID == 0 {
|
||||
return
|
||||
}
|
||||
portalUsers, err := models.GetPortalUsersByClienteID(factura.ClienteID)
|
||||
if err != nil || len(portalUsers) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
clienteNombre := ""
|
||||
if factura.Cliente.Nombre != "" {
|
||||
clienteNombre = factura.Cliente.Nombre
|
||||
} else if factura.Cliente.Empresa != "" {
|
||||
clienteNombre = factura.Cliente.Empresa
|
||||
}
|
||||
|
||||
for _, u := range portalUsers {
|
||||
u := u // capture
|
||||
portalURL := "/portal/dashboard"
|
||||
titulo := fmt.Sprintf("Nueva factura disponible: %s", factura.Numero)
|
||||
cuerpo := fmt.Sprintf("Factura %s por %s %s", factura.Numero, factura.Moneda, fmt.Sprintf("%.2f", factura.Monto))
|
||||
|
||||
if cfg.CanalSistema {
|
||||
_ = models.CreateSistemaNotif(&models.SistemaNotificacion{
|
||||
TipoUsuario: "portal_user",
|
||||
UsuarioID: u.ID,
|
||||
Titulo: titulo,
|
||||
Cuerpo: cuerpo,
|
||||
Url: portalURL,
|
||||
Icono: "🧾",
|
||||
})
|
||||
}
|
||||
if cfg.CanalEmail && u.Email != "" {
|
||||
go SendFacturaSubidaPortalUser(u.Email, u.Nombre, clienteNombre, factura.Numero, factura.Monto, factura.Moneda, portalURL)
|
||||
}
|
||||
if cfg.CanalTelegram && u.TelegramChatID != "" {
|
||||
ts := NewTelegramService()
|
||||
msg := fmt.Sprintf("🧾 <b>Nueva factura disponible</b>\nNúmero: <b>%s</b>\nMonto: %s %.2f\n\n🔗 %s",
|
||||
factura.Numero, factura.Moneda, factura.Monto, portalURL)
|
||||
if err := ts.SendMessageWithToken(u.TelegramChatID, msg, getAdminBotToken()); err != nil {
|
||||
log.Printf("[Notif] Error telegram portal_user %d: %v", u.ID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Printf("[Notif] DispatchFacturaSubida factura=%d clientes notificados=%d", factura.ID, len(portalUsers))
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ShieldClient cliente HTTP para la API de USITE Shield.
|
||||
type ShieldClient struct {
|
||||
baseURL string
|
||||
adminToken string
|
||||
http *http.Client
|
||||
}
|
||||
|
||||
func NewShieldClient(baseURL, adminToken string) *ShieldClient {
|
||||
if baseURL == "" {
|
||||
baseURL = "https://api-shield.u-s.app"
|
||||
}
|
||||
return &ShieldClient{
|
||||
baseURL: baseURL,
|
||||
adminToken: adminToken,
|
||||
http: &http.Client{Timeout: 15 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ShieldClient) do(method, path string, body interface{}) ([]byte, int, error) {
|
||||
var bodyReader io.Reader
|
||||
if body != nil {
|
||||
b, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
bodyReader = bytes.NewReader(b)
|
||||
}
|
||||
req, err := http.NewRequest(method, s.baseURL+path, bodyReader)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if s.adminToken != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+s.adminToken)
|
||||
}
|
||||
resp, err := s.http.Do(req)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
return data, resp.StatusCode, err
|
||||
}
|
||||
|
||||
// ─── Health ──────────────────────────────────────────────────────────────────
|
||||
|
||||
func (s *ShieldClient) Health() (map[string]interface{}, error) {
|
||||
data, _, err := s.do("GET", "/health", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out map[string]interface{}
|
||||
return out, json.Unmarshal(data, &out)
|
||||
}
|
||||
|
||||
// ─── Extension version ───────────────────────────────────────────────────────
|
||||
|
||||
func (s *ShieldClient) ExtensionVersion() (map[string]interface{}, error) {
|
||||
data, _, err := s.do("GET", "/api/extension/version", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out map[string]interface{}
|
||||
return out, json.Unmarshal(data, &out)
|
||||
}
|
||||
|
||||
// ─── Logs ────────────────────────────────────────────────────────────────────
|
||||
|
||||
func (s *ShieldClient) Logs(limit int, path, method string) (map[string]interface{}, error) {
|
||||
q := fmt.Sprintf("/api/admin/logs?limit=%d", limit)
|
||||
if path != "" {
|
||||
q += "&path=" + path
|
||||
}
|
||||
if method != "" {
|
||||
q += "&method=" + method
|
||||
}
|
||||
data, _, err := s.do("GET", q, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out map[string]interface{}
|
||||
return out, json.Unmarshal(data, &out)
|
||||
}
|
||||
|
||||
func (s *ShieldClient) LogStats() (map[string]interface{}, error) {
|
||||
data, _, err := s.do("GET", "/api/admin/logs/stats", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out map[string]interface{}
|
||||
return out, json.Unmarshal(data, &out)
|
||||
}
|
||||
|
||||
// ─── Review requests ─────────────────────────────────────────────────────────
|
||||
|
||||
func (s *ShieldClient) ReviewRequests(status string) (map[string]interface{}, error) {
|
||||
if status == "" {
|
||||
status = "pending"
|
||||
}
|
||||
data, _, err := s.do("GET", "/api/admin/review-requests?status="+status, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out map[string]interface{}
|
||||
return out, json.Unmarshal(data, &out)
|
||||
}
|
||||
|
||||
func (s *ShieldClient) ApproveRequest(id, note, reason string) (map[string]interface{}, error) {
|
||||
body := map[string]string{"note": note, "reason": reason}
|
||||
data, code, err := s.do("PUT", "/api/admin/review-requests/"+id+"/approve", body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if code >= 400 {
|
||||
return nil, fmt.Errorf("shield API error %d: %s", code, string(data))
|
||||
}
|
||||
var out map[string]interface{}
|
||||
json.Unmarshal(data, &out)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *ShieldClient) RejectRequest(id, note string) (map[string]interface{}, error) {
|
||||
body := map[string]string{"note": note}
|
||||
data, code, err := s.do("PUT", "/api/admin/review-requests/"+id+"/reject", body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if code >= 400 {
|
||||
return nil, fmt.Errorf("shield API error %d: %s", code, string(data))
|
||||
}
|
||||
var out map[string]interface{}
|
||||
json.Unmarshal(data, &out)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ─── Whitelist ───────────────────────────────────────────────────────────────
|
||||
|
||||
func (s *ShieldClient) Whitelist() (map[string]interface{}, error) {
|
||||
data, _, err := s.do("GET", "/api/admin/whitelist", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out map[string]interface{}
|
||||
return out, json.Unmarshal(data, &out)
|
||||
}
|
||||
|
||||
func (s *ShieldClient) AddWhitelist(domain, reason, addedBy string) (map[string]interface{}, error) {
|
||||
body := map[string]string{"domain": domain, "reason": reason, "added_by": addedBy}
|
||||
data, code, err := s.do("POST", "/api/admin/whitelist", body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if code >= 400 {
|
||||
return nil, fmt.Errorf("shield API error %d: %s", code, string(data))
|
||||
}
|
||||
var out map[string]interface{}
|
||||
json.Unmarshal(data, &out)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *ShieldClient) DeleteWhitelist(domain string) error {
|
||||
data, code, err := s.do("DELETE", "/api/admin/whitelist/"+domain, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if code >= 400 {
|
||||
return fmt.Errorf("shield API error %d: %s", code, string(data))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ─── Blacklist ───────────────────────────────────────────────────────────────
|
||||
|
||||
func (s *ShieldClient) Blacklist() (map[string]interface{}, error) {
|
||||
data, _, err := s.do("GET", "/api/admin/blacklist", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out map[string]interface{}
|
||||
return out, json.Unmarshal(data, &out)
|
||||
}
|
||||
|
||||
func (s *ShieldClient) AddBlacklist(domain, reason, addedBy string) (map[string]interface{}, error) {
|
||||
body := map[string]string{"domain": domain, "reason": reason, "added_by": addedBy}
|
||||
data, code, err := s.do("POST", "/api/admin/blacklist", body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if code >= 400 {
|
||||
return nil, fmt.Errorf("shield API error %d: %s", code, string(data))
|
||||
}
|
||||
var out map[string]interface{}
|
||||
json.Unmarshal(data, &out)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *ShieldClient) DeleteBlacklist(domain string) error {
|
||||
data, code, err := s.do("DELETE", "/api/admin/blacklist/"+domain, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if code >= 400 {
|
||||
return fmt.Errorf("shield API error %d: %s", code, string(data))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ─── Reputación ──────────────────────────────────────────────────────────────
|
||||
|
||||
func (s *ShieldClient) Reputation(domain string) (map[string]interface{}, error) {
|
||||
data, _, err := s.do("GET", "/api/reputation/"+domain, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out map[string]interface{}
|
||||
return out, json.Unmarshal(data, &out)
|
||||
}
|
||||
|
||||
func (s *ShieldClient) AdjustScore(domain string, votesReal, votesFake int) (map[string]interface{}, error) {
|
||||
body := map[string]int{"votes_real": votesReal, "votes_fake": votesFake}
|
||||
data, code, err := s.do("PUT", "/api/admin/reputation/"+domain+"/score", body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if code >= 400 {
|
||||
return nil, fmt.Errorf("shield API error %d: %s", code, string(data))
|
||||
}
|
||||
var out map[string]interface{}
|
||||
json.Unmarshal(data, &out)
|
||||
return out, nil
|
||||
}
|
||||
@@ -106,6 +106,7 @@ const EVENTOS = [
|
||||
{ evento: 'ticket_nuevo', destinatario: 'admin', label: 'Ticket nuevo', icon: '🎫' },
|
||||
{ evento: 'ticket_respuesta_cliente', destinatario: 'admin', label: 'Respuesta del cliente', icon: '💬' },
|
||||
{ evento: 'ticket_respuesta_admin', destinatario: 'portal_user', label: 'Respuesta del admin', icon: '💬' },
|
||||
{ evento: 'factura_subida', destinatario: 'portal_user', label: 'Factura subida / disponible', icon: '🧾' },
|
||||
// Próximos eventos (deshabilitados por ahora):
|
||||
// { evento: 'factura_emitida', destinatario: 'portal_user', label: 'Factura emitida', icon: '🧾' },
|
||||
// { evento: 'avance_publicado', destinatario: 'portal_user', label: 'Avance publicado', icon: '📦' },
|
||||
|
||||
@@ -0,0 +1,649 @@
|
||||
<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':'bg-blue-100 text-blue-700'"
|
||||
x-text="r.request_type==='whitelist'?'Lista blanca':'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: 'pending',
|
||||
// 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>
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
||||
"github.com/sujit-baniya/fiber-boilerplate/pkg/services"
|
||||
)
|
||||
|
||||
func FacturasIndex(c *fiber.Ctx) error {
|
||||
@@ -185,6 +186,10 @@ func UploadFacturaPDF(c *fiber.Ctx) error {
|
||||
if err := models.UpdateFacturaArchivo(uint(id), savePath, file.Filename); err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
// Notificar al cliente
|
||||
if f, err := models.GetFacturaByID(uint(id)); err == nil {
|
||||
go services.DispatchFacturaSubida(f)
|
||||
}
|
||||
return c.JSON(fiber.Map{"ok": true, "archivo": savePath})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
||||
"github.com/sujit-baniya/fiber-boilerplate/pkg/services"
|
||||
)
|
||||
|
||||
// shieldClient construye el cliente Shield desde la config activa.
|
||||
func shieldClient() (*services.ShieldClient, error) {
|
||||
cfg, err := models.GetShieldConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return services.NewShieldClient(cfg.BaseUrl, cfg.AdminToken), nil
|
||||
}
|
||||
|
||||
// ─── Vista principal ─────────────────────────────────────────────────────────
|
||||
|
||||
func ShieldIndex(c *fiber.Ctx) error {
|
||||
return c.Render("shield", fiber.Map{
|
||||
"user": c.Locals("user"),
|
||||
"modules": c.Locals("userModules"),
|
||||
}, "layouts/main")
|
||||
}
|
||||
|
||||
// ─── Configuración ───────────────────────────────────────────────────────────
|
||||
|
||||
func LoadShieldConfig(c *fiber.Ctx) error {
|
||||
cfg, err := models.GetShieldConfig()
|
||||
if err != nil {
|
||||
return c.JSON(fiber.Map{"base_url": "https://api-shield.u-s.app", "admin_token": "", "activo": true, "nota": ""})
|
||||
}
|
||||
return c.JSON(cfg)
|
||||
}
|
||||
|
||||
func SaveShieldConfig(c *fiber.Ctx) error {
|
||||
var req models.ShieldConfig
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
if req.BaseUrl == "" {
|
||||
req.BaseUrl = "https://api-shield.u-s.app"
|
||||
}
|
||||
if err := models.UpsertShieldConfig(&req); err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(fiber.Map{"ok": true})
|
||||
}
|
||||
|
||||
// ─── Health ──────────────────────────────────────────────────────────────────
|
||||
|
||||
func ShieldHealth(c *fiber.Ctx) error {
|
||||
client, err := shieldClient()
|
||||
if err != nil {
|
||||
return c.JSON(fiber.Map{"status": "sin configurar", "db": "-", "error": "No hay configuración Shield activa"})
|
||||
}
|
||||
data, err := client.Health()
|
||||
if err != nil {
|
||||
return c.JSON(fiber.Map{"status": "offline", "db": "-", "error": err.Error()})
|
||||
}
|
||||
return c.JSON(data)
|
||||
}
|
||||
|
||||
func ShieldExtensionVersion(c *fiber.Ctx) error {
|
||||
client, err := shieldClient()
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": "Sin configuración"})
|
||||
}
|
||||
data, err := client.ExtensionVersion()
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(data)
|
||||
}
|
||||
|
||||
// ─── Logs ────────────────────────────────────────────────────────────────────
|
||||
|
||||
func ShieldLogs(c *fiber.Ctx) error {
|
||||
client, err := shieldClient()
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": "Sin configuración"})
|
||||
}
|
||||
limit, _ := strconv.Atoi(c.Query("limit", "100"))
|
||||
data, err := client.Logs(limit, c.Query("path"), c.Query("method"))
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(data)
|
||||
}
|
||||
|
||||
func ShieldLogStats(c *fiber.Ctx) error {
|
||||
client, err := shieldClient()
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": "Sin configuración"})
|
||||
}
|
||||
data, err := client.LogStats()
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(data)
|
||||
}
|
||||
|
||||
// ─── Review requests ─────────────────────────────────────────────────────────
|
||||
|
||||
func ShieldReviewRequests(c *fiber.Ctx) error {
|
||||
client, err := shieldClient()
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": "Sin configuración"})
|
||||
}
|
||||
data, err := client.ReviewRequests(c.Query("status", "pending"))
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(data)
|
||||
}
|
||||
|
||||
func ShieldApproveRequest(c *fiber.Ctx) error {
|
||||
client, err := shieldClient()
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": "Sin configuración"})
|
||||
}
|
||||
id := c.Params("id")
|
||||
var body struct {
|
||||
Note string `json:"note"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
c.BodyParser(&body)
|
||||
data, err := client.ApproveRequest(id, body.Note, body.Reason)
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(data)
|
||||
}
|
||||
|
||||
func ShieldRejectRequest(c *fiber.Ctx) error {
|
||||
client, err := shieldClient()
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": "Sin configuración"})
|
||||
}
|
||||
id := c.Params("id")
|
||||
var body struct {
|
||||
Note string `json:"note"`
|
||||
}
|
||||
c.BodyParser(&body)
|
||||
data, err := client.RejectRequest(id, body.Note)
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(data)
|
||||
}
|
||||
|
||||
// ─── Whitelist ───────────────────────────────────────────────────────────────
|
||||
|
||||
func ShieldWhitelist(c *fiber.Ctx) error {
|
||||
client, err := shieldClient()
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": "Sin configuración"})
|
||||
}
|
||||
data, err := client.Whitelist()
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(data)
|
||||
}
|
||||
|
||||
func ShieldAddWhitelist(c *fiber.Ctx) error {
|
||||
client, err := shieldClient()
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": "Sin configuración"})
|
||||
}
|
||||
var body struct {
|
||||
Domain string `json:"domain"`
|
||||
Reason string `json:"reason"`
|
||||
AddedBy string `json:"added_by"`
|
||||
}
|
||||
if err := c.BodyParser(&body); err != nil || body.Domain == "" {
|
||||
return c.Status(400).JSON(fiber.Map{"error": "domain requerido"})
|
||||
}
|
||||
if body.AddedBy == "" {
|
||||
body.AddedBy = "admin"
|
||||
}
|
||||
data, err := client.AddWhitelist(body.Domain, body.Reason, body.AddedBy)
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(data)
|
||||
}
|
||||
|
||||
func ShieldDeleteWhitelist(c *fiber.Ctx) error {
|
||||
client, err := shieldClient()
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": "Sin configuración"})
|
||||
}
|
||||
domain := c.Params("domain")
|
||||
if err := client.DeleteWhitelist(domain); err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(fiber.Map{"ok": true})
|
||||
}
|
||||
|
||||
// ─── Blacklist ───────────────────────────────────────────────────────────────
|
||||
|
||||
func ShieldBlacklist(c *fiber.Ctx) error {
|
||||
client, err := shieldClient()
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": "Sin configuración"})
|
||||
}
|
||||
data, err := client.Blacklist()
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(data)
|
||||
}
|
||||
|
||||
func ShieldAddBlacklist(c *fiber.Ctx) error {
|
||||
client, err := shieldClient()
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": "Sin configuración"})
|
||||
}
|
||||
var body struct {
|
||||
Domain string `json:"domain"`
|
||||
Reason string `json:"reason"`
|
||||
AddedBy string `json:"added_by"`
|
||||
}
|
||||
if err := c.BodyParser(&body); err != nil || body.Domain == "" {
|
||||
return c.Status(400).JSON(fiber.Map{"error": "domain requerido"})
|
||||
}
|
||||
if body.AddedBy == "" {
|
||||
body.AddedBy = "admin"
|
||||
}
|
||||
data, err := client.AddBlacklist(body.Domain, body.Reason, body.AddedBy)
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(data)
|
||||
}
|
||||
|
||||
func ShieldDeleteBlacklist(c *fiber.Ctx) error {
|
||||
client, err := shieldClient()
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": "Sin configuración"})
|
||||
}
|
||||
domain := c.Params("domain")
|
||||
if err := client.DeleteBlacklist(domain); err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(fiber.Map{"ok": true})
|
||||
}
|
||||
|
||||
// ─── Reputación ──────────────────────────────────────────────────────────────
|
||||
|
||||
func ShieldReputation(c *fiber.Ctx) error {
|
||||
client, err := shieldClient()
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": "Sin configuración"})
|
||||
}
|
||||
domain := c.Params("domain")
|
||||
data, err := client.Reputation(domain)
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(data)
|
||||
}
|
||||
|
||||
func ShieldAdjustScore(c *fiber.Ctx) error {
|
||||
client, err := shieldClient()
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": "Sin configuración"})
|
||||
}
|
||||
domain := c.Params("domain")
|
||||
var body struct {
|
||||
VotesReal int `json:"votes_real"`
|
||||
VotesFake int `json:"votes_fake"`
|
||||
}
|
||||
if err := c.BodyParser(&body); err != nil {
|
||||
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
data, err := client.AdjustScore(domain, body.VotesReal, body.VotesFake)
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(data)
|
||||
}
|
||||
@@ -154,6 +154,26 @@ func UserRoutes(app fiber.Router) {
|
||||
protected.Get("/cloudflare/zones/:zone_id/ssl", controllers.GetCloudflareSSL)
|
||||
protected.Get("/cloudflare/zones/:zone_id/firewall", controllers.GetCloudflareFirewall)
|
||||
|
||||
// ─── USITE Shield ─────────────────────────────────────────────────────────
|
||||
protected.Get("/shield", middlewares.MenuMiddleware, controllers.ShieldIndex)
|
||||
protected.Get("/loadshield", controllers.LoadShieldConfig)
|
||||
protected.Post("/saveshield", controllers.SaveShieldConfig)
|
||||
protected.Get("/shield/health", controllers.ShieldHealth)
|
||||
protected.Get("/shield/extension-version", controllers.ShieldExtensionVersion)
|
||||
protected.Get("/shield/logs", controllers.ShieldLogs)
|
||||
protected.Get("/shield/logs/stats", controllers.ShieldLogStats)
|
||||
protected.Get("/shield/review-requests", controllers.ShieldReviewRequests)
|
||||
protected.Put("/shield/review-requests/:id/approve", controllers.ShieldApproveRequest)
|
||||
protected.Put("/shield/review-requests/:id/reject", controllers.ShieldRejectRequest)
|
||||
protected.Get("/shield/whitelist", controllers.ShieldWhitelist)
|
||||
protected.Post("/shield/whitelist", controllers.ShieldAddWhitelist)
|
||||
protected.Delete("/shield/whitelist/:domain", controllers.ShieldDeleteWhitelist)
|
||||
protected.Get("/shield/blacklist", controllers.ShieldBlacklist)
|
||||
protected.Post("/shield/blacklist", controllers.ShieldAddBlacklist)
|
||||
protected.Delete("/shield/blacklist/:domain", controllers.ShieldDeleteBlacklist)
|
||||
protected.Get("/shield/reputation/:domain", controllers.ShieldReputation)
|
||||
protected.Put("/shield/reputation/:domain/score", controllers.ShieldAdjustScore)
|
||||
|
||||
// ─── Pasarelas de Pago ────────────────────────────────────────────
|
||||
protected.Get("/pasarelas-pago", middlewares.MenuMiddleware, controllers.PasarelasPage)
|
||||
// Bold
|
||||
|
||||
Reference in New Issue
Block a user