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.
This commit is contained in:
@@ -76,12 +76,35 @@ namespace ChatbotApi.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("logs")]
|
||||
public async Task<IActionResult> GetConversationLogs()
|
||||
public async Task<IActionResult> GetConversationLogs(
|
||||
[FromQuery] DateTime? startDate,
|
||||
[FromQuery] DateTime? endDate,
|
||||
[FromQuery] string? search)
|
||||
{
|
||||
// Limitamos a 200 para evitar sobrecarga
|
||||
var logs = await _context.ConversacionLogs
|
||||
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(200)
|
||||
.Take(500)
|
||||
.ToListAsync();
|
||||
return Ok(logs);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@ public class LoginRequest
|
||||
[MaxLength(100)]
|
||||
public required string Password { get; set; }
|
||||
}
|
||||
public class LoginResponse { public required string Token { get; set; } }
|
||||
// [SEGURIDAD] LoginResponse ya no es necesario si usamos solo cookies, pero podriamos dejar un mensaje de exito.
|
||||
public class LoginResponse { public string Message { get; set; } = "Login exitoso"; }
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
@@ -41,12 +42,31 @@ public class AuthController : ControllerBase
|
||||
if (user != null && await _userManager.CheckPasswordAsync(user, loginRequest.Password))
|
||||
{
|
||||
var token = GenerateJwtToken(user);
|
||||
return Ok(new LoginResponse { Token = token });
|
||||
|
||||
// [SEGURIDAD] Setear Cookie HttpOnly
|
||||
var cookieOptions = new CookieOptions
|
||||
{
|
||||
HttpOnly = true,
|
||||
Secure = true, // Requiere HTTPS
|
||||
SameSite = SameSiteMode.Strict,
|
||||
Expires = DateTime.UtcNow.AddHours(8)
|
||||
};
|
||||
|
||||
Response.Cookies.Append("X-Access-Token", token, cookieOptions);
|
||||
|
||||
return Ok(new LoginResponse());
|
||||
}
|
||||
|
||||
return Unauthorized("Credenciales inválidas.");
|
||||
}
|
||||
|
||||
[HttpPost("logout")]
|
||||
public IActionResult Logout()
|
||||
{
|
||||
Response.Cookies.Delete("X-Access-Token");
|
||||
return Ok(new { message = "Sesión cerrada" });
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
// [SEGURIDAD] Endpoint solo para desarrollo
|
||||
[HttpPost("setup-admin")]
|
||||
|
||||
@@ -57,7 +57,7 @@ namespace ChatbotApi.Controllers
|
||||
try
|
||||
{
|
||||
await _context.SaveChangesAsync();
|
||||
_cache.Remove(CacheKey); // Invalidate cache
|
||||
_cache.Remove(CacheKey); // Invalidar caché
|
||||
}
|
||||
catch (DbUpdateConcurrencyException)
|
||||
{
|
||||
@@ -82,7 +82,7 @@ namespace ChatbotApi.Controllers
|
||||
systemPrompt.UpdatedAt = DateTime.UtcNow;
|
||||
_context.SystemPrompts.Add(systemPrompt);
|
||||
await _context.SaveChangesAsync();
|
||||
_cache.Remove(CacheKey); // Invalidate cache
|
||||
_cache.Remove(CacheKey); // Invalidar caché
|
||||
|
||||
return CreatedAtAction("GetSystemPrompt", new { id = systemPrompt.Id }, systemPrompt);
|
||||
}
|
||||
@@ -99,7 +99,7 @@ namespace ChatbotApi.Controllers
|
||||
|
||||
_context.SystemPrompts.Remove(systemPrompt);
|
||||
await _context.SaveChangesAsync();
|
||||
_cache.Remove(CacheKey); // Invalidate cache
|
||||
_cache.Remove(CacheKey); // Invalidar caché
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
@@ -117,7 +117,7 @@ namespace ChatbotApi.Controllers
|
||||
systemPrompt.IsActive = !systemPrompt.IsActive;
|
||||
systemPrompt.UpdatedAt = DateTime.UtcNow;
|
||||
await _context.SaveChangesAsync();
|
||||
_cache.Remove(CacheKey); // Invalidate cache
|
||||
_cache.Remove(CacheKey); // Invalidar caché
|
||||
|
||||
return Ok(new { IsActive = systemPrompt.IsActive });
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ builder.Services.AddCors(options =>
|
||||
"http://localhost:5174",
|
||||
"http://localhost:5175")
|
||||
.AllowAnyHeader()
|
||||
// [SEGURIDAD] Solo permitimos los verbos necesarios. Bloqueamos TRACE, HEAD, etc.
|
||||
.AllowCredentials() // [SEGURIDAD] Necesario para Cookies HttpOnly
|
||||
.WithMethods("GET", "POST", "PUT", "DELETE", "OPTIONS");
|
||||
});
|
||||
});
|
||||
@@ -85,6 +85,19 @@ builder.Services.AddAuthentication(options =>
|
||||
)),
|
||||
ClockSkew = TimeSpan.Zero // Token expira exactamente cuando dice
|
||||
};
|
||||
|
||||
// [SEGURIDAD] Evento para permitir lectura de Token desde Cookie
|
||||
options.Events = new JwtBearerEvents
|
||||
{
|
||||
OnMessageReceived = context =>
|
||||
{
|
||||
if (context.Request.Cookies.ContainsKey("X-Access-Token"))
|
||||
{
|
||||
context.Token = context.Request.Cookies["X-Access-Token"];
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// [SEGURIDAD] RATE LIMITING AVANZADO
|
||||
|
||||
Reference in New Issue
Block a user