Files
Chatbot-ElDia/ChatbotApi/Constrollers/AdminController.cs
dmolinari 5c97614e4f HttpOnly Cookies y Filtros Avanzados
1. Seguridad: Cookies HttpOnly
Backend (ChatbotApi):
AuthController.cs
: Ahora setea una cookie HttpOnly, Secure y SameSite=Strict llamada X-Access-Token en lugar de devolver el token en el cuerpo de la respuesta.
AuthController.cs
: Añadido endpoint logout para invalidar la cookie.
Program.cs
: Configurado JwtBearer para leer el token desde la cookie si está presente.
Frontend (chatbot-admin):
apiClient.ts
: Configurado con withCredentials: true para enviar cookies automáticamente. Eliminado el interceptor de localStorage.
Login.tsx
: Eliminado manejo de token manual. Ahora solo comprueba éxito (200 OK).
App.tsx
: Refactorizado para comprobar autenticación mediante una petición a /api/admin/contexto al inicio, en lugar de leer localStorage.
2. Filtros y Búsqueda
Logs (
AdminController.cs
 &
LogsViewer.tsx
):
Implementado filtrado en servidor por Fecha Inicio, Fecha Fin y Búsqueda de texto.
Frontend actualizado con selectores de fecha y barra de búsqueda.
Contexto y Fuentes (
ContextManager.tsx
 &
SourceManager.tsx
):
Añadida barra de búsqueda en el cliente para filtrar rápidamente por nombre, valor o descripción.
2025-12-05 14:03:27 -03:00

178 lines
6.3 KiB
C#

using ChatbotApi.Data.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Caching.Memory;
using ChatbotApi.Services;
using Microsoft.EntityFrameworkCore;
namespace ChatbotApi.Controllers
{
[ApiController]
[Route("api/[controller]")]
[Authorize] // Requiere Token JWT válido
public class AdminController : ControllerBase
{
private readonly AppContexto _context;
private readonly IMemoryCache _cache;
public AdminController(AppContexto context, IMemoryCache cache)
{
_context = context;
_cache = cache;
}
// --- CONTEXTO ITEMS (Sin cambios mayores de seguridad más allá de Authorize) ---
[HttpGet("contexto")]
public async Task<IActionResult> GetAllContextoItems()
{
var items = await _context.ContextoItems.OrderBy(i => i.Clave).ToListAsync();
return Ok(items);
}
[HttpPost("contexto")]
public async Task<IActionResult> CreateContextoItem([FromBody] ContextoItem item)
{
// [SEGURIDAD] Validación de entrada
if (!ModelState.IsValid) return BadRequest(ModelState);
if (await _context.ContextoItems.AnyAsync(i => i.Clave == item.Clave))
{
return BadRequest("La clave ya existe.");
}
item.FechaActualizacion = DateTime.UtcNow;
_context.ContextoItems.Add(item);
await _context.SaveChangesAsync();
_cache.Remove(CacheKeys.KnowledgeItems);
return CreatedAtAction(nameof(GetAllContextoItems), new { id = item.Id }, item);
}
[HttpPut("contexto/{id}")]
public async Task<IActionResult> UpdateContextoItem(int id, [FromBody] ContextoItem item)
{
if (id != item.Id) return BadRequest();
var existingItem = await _context.ContextoItems.FindAsync(id);
if (existingItem == null) return NotFound();
existingItem.Valor = item.Valor;
existingItem.Descripcion = item.Descripcion;
existingItem.FechaActualizacion = DateTime.UtcNow;
await _context.SaveChangesAsync();
_cache.Remove(CacheKeys.KnowledgeItems);
return NoContent();
}
[HttpDelete("contexto/{id}")]
public async Task<IActionResult> DeleteContextoItem(int id)
{
var item = await _context.ContextoItems.FindAsync(id);
if (item == null) return NotFound();
_context.ContextoItems.Remove(item);
await _context.SaveChangesAsync();
_cache.Remove(CacheKeys.KnowledgeItems);
return NoContent();
}
[HttpGet("logs")]
public async Task<IActionResult> GetConversationLogs(
[FromQuery] DateTime? startDate,
[FromQuery] DateTime? endDate,
[FromQuery] string? search)
{
var query = _context.ConversacionLogs.AsQueryable();
if (startDate.HasValue)
{
query = query.Where(l => l.Fecha >= startDate.Value);
}
if (endDate.HasValue)
{
// Ajustamos al final del día si es necesario, o asumimos fecha exacta
query = query.Where(l => l.Fecha <= endDate.Value);
}
if (!string.IsNullOrWhiteSpace(search))
{
query = query.Where(l =>
l.UsuarioMensaje.Contains(search) ||
l.BotRespuesta.Contains(search));
}
// Limitamos a 500 para evitar sobrecarga pero permitiendo ver resultados de búsqueda
var logs = await query
.OrderByDescending(log => log.Fecha)
.Take(500)
.ToListAsync();
return Ok(logs);
}
// --- FUENTES DE CONTEXTO (APLICAMOS LA SEGURIDAD SSRF) ---
[HttpGet("fuentes")]
public async Task<IActionResult> GetAllFuentes()
{
var fuentes = await _context.FuentesDeContexto.OrderBy(f => f.Nombre).ToListAsync();
return Ok(fuentes);
}
[HttpPost("fuentes")]
public async Task<IActionResult> CreateFuente([FromBody] FuenteContexto fuente)
{
if (!ModelState.IsValid) return BadRequest(ModelState);
// [SEGURIDAD] Validar que la URL no sea interna/maliciosa ANTES de guardarla
if (!await UrlSecurity.IsSafeUrlAsync(fuente.Url))
{
return BadRequest("La URL proporcionada no es válida o apunta a una dirección interna restringida.");
}
_context.FuentesDeContexto.Add(fuente);
await _context.SaveChangesAsync();
_cache.Remove(CacheKeys.FuentesDeContexto);
return CreatedAtAction(nameof(GetAllFuentes), new { id = fuente.Id }, fuente);
}
[HttpPut("fuentes/{id}")]
public async Task<IActionResult> UpdateFuente(int id, [FromBody] FuenteContexto fuente)
{
if (id != fuente.Id) return BadRequest();
// [SEGURIDAD] Validar también en la actualización
if (!await UrlSecurity.IsSafeUrlAsync(fuente.Url))
{
return BadRequest("La URL proporcionada no es válida o apunta a una dirección interna restringida.");
}
_context.Entry(fuente).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!_context.FuentesDeContexto.Any(e => e.Id == id)) return NotFound();
else throw;
}
_cache.Remove(CacheKeys.FuentesDeContexto);
return NoContent();
}
[HttpDelete("fuentes/{id}")]
public async Task<IActionResult> DeleteFuente(int id)
{
var fuente = await _context.FuentesDeContexto.FindAsync(id);
if (fuente == null) return NotFound();
_context.FuentesDeContexto.Remove(fuente);
await _context.SaveChangesAsync();
_cache.Remove(CacheKeys.FuentesDeContexto);
return NoContent();
}
}
}