Fix: Se refinan IA y Estructuras
This commit is contained in:
178
ChatbotApi/Controllers/AdminController.cs
Normal file
178
ChatbotApi/Controllers/AdminController.cs
Normal file
@@ -0,0 +1,178 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
124
ChatbotApi/Controllers/AuthController.cs
Normal file
124
ChatbotApi/Controllers/AuthController.cs
Normal file
@@ -0,0 +1,124 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
|
||||
public class LoginRequest
|
||||
{
|
||||
[Required]
|
||||
[MaxLength(100)]
|
||||
public required string Username { get; set; }
|
||||
|
||||
[Required]
|
||||
[MaxLength(100)]
|
||||
public required string Password { 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]")]
|
||||
public class AuthController : ControllerBase
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly UserManager<IdentityUser> _userManager;
|
||||
|
||||
public AuthController(IConfiguration configuration, UserManager<IdentityUser> userManager)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
[HttpPost("login")]
|
||||
[EnableRateLimiting("login-limit")]
|
||||
public async Task<IActionResult> Login([FromBody] LoginRequest loginRequest)
|
||||
{
|
||||
var user = await _userManager.FindByNameAsync(loginRequest.Username);
|
||||
|
||||
if (user != null && await _userManager.CheckPasswordAsync(user, loginRequest.Password))
|
||||
{
|
||||
var token = GenerateJwtToken(user);
|
||||
|
||||
// [SEGURIDAD] Setear Cookie HttpOnly
|
||||
var cookieOptions = new CookieOptions
|
||||
{
|
||||
HttpOnly = true,
|
||||
Secure = Request.IsHttps, // Dinámico: true si es HTTPS, false si es HTTP
|
||||
SameSite = SameSiteMode.Lax, // Permite navegación entre puertos/IPs más facil
|
||||
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" });
|
||||
}
|
||||
|
||||
[HttpGet("status")]
|
||||
[Microsoft.AspNetCore.Authorization.AllowAnonymous]
|
||||
public IActionResult GetStatus()
|
||||
{
|
||||
return Ok(new { isAuthenticated = User.Identity?.IsAuthenticated ?? false });
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
// [SEGURIDAD] Endpoint solo para desarrollo
|
||||
[HttpPost("setup-admin")]
|
||||
public async Task<IActionResult> SetupAdminUser()
|
||||
{
|
||||
var adminUser = await _userManager.FindByNameAsync("admin");
|
||||
if (adminUser == null)
|
||||
{
|
||||
adminUser = new IdentityUser
|
||||
{
|
||||
UserName = "admin",
|
||||
Email = "tecnica@eldia.com",
|
||||
};
|
||||
// En producción usar Secrets, no hardcoded
|
||||
var result = await _userManager.CreateAsync(adminUser, "Diagonal423");
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
return Ok("Usuario administrador creado exitosamente.");
|
||||
}
|
||||
return BadRequest(result.Errors);
|
||||
}
|
||||
return Ok("El usuario administrador ya existe.");
|
||||
}
|
||||
#endif
|
||||
|
||||
private string GenerateJwtToken(IdentityUser user)
|
||||
{
|
||||
var jwtKey = _configuration["Jwt:Key"] ?? throw new InvalidOperationException("La clave JWT no está configurada.");
|
||||
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey));
|
||||
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
|
||||
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim(JwtRegisteredClaimNames.Sub, user.UserName!),
|
||||
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
|
||||
};
|
||||
|
||||
var token = new JwtSecurityToken(
|
||||
issuer: _configuration["Jwt:Issuer"],
|
||||
audience: _configuration["Jwt:Audience"],
|
||||
claims: claims,
|
||||
expires: DateTime.UtcNow.AddHours(8),
|
||||
signingCredentials: credentials);
|
||||
|
||||
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||
}
|
||||
}
|
||||
31
ChatbotApi/Controllers/ChatController.cs
Normal file
31
ChatbotApi/Controllers/ChatController.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using ChatbotApi.Data.Models;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using System.Runtime.CompilerServices;
|
||||
using ChatbotApi.Services;
|
||||
|
||||
namespace ChatbotApi.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class ChatController : ControllerBase
|
||||
{
|
||||
private readonly IChatService _chatService;
|
||||
private readonly ILogger<ChatController> _logger;
|
||||
|
||||
public ChatController(IChatService chatService, ILogger<ChatController> logger)
|
||||
{
|
||||
_chatService = chatService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[HttpPost("stream-message")]
|
||||
[EnableRateLimiting("fixed")]
|
||||
public IAsyncEnumerable<string> StreamMessage(
|
||||
[FromBody] ChatRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return _chatService.StreamMessageAsync(request, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
130
ChatbotApi/Controllers/SystemPromptsController.cs
Normal file
130
ChatbotApi/Controllers/SystemPromptsController.cs
Normal file
@@ -0,0 +1,130 @@
|
||||
using ChatbotApi.Data.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
|
||||
namespace ChatbotApi.Controllers
|
||||
{
|
||||
[Authorize]
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
public class SystemPromptsController : ControllerBase
|
||||
{
|
||||
private readonly AppContexto _context;
|
||||
private readonly IMemoryCache _cache;
|
||||
private const string CacheKey = "ActiveSystemPrompts";
|
||||
|
||||
public SystemPromptsController(AppContexto context, IMemoryCache cache)
|
||||
{
|
||||
_context = context;
|
||||
_cache = cache;
|
||||
}
|
||||
|
||||
// GET: api/SystemPrompts
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<SystemPrompt>>> GetSystemPrompts()
|
||||
{
|
||||
return await _context.SystemPrompts.OrderByDescending(p => p.CreatedAt).ToListAsync();
|
||||
}
|
||||
|
||||
// GET: api/SystemPrompts/5
|
||||
[HttpGet("{id}")]
|
||||
public async Task<ActionResult<SystemPrompt>> GetSystemPrompt(int id)
|
||||
{
|
||||
var systemPrompt = await _context.SystemPrompts.FindAsync(id);
|
||||
|
||||
if (systemPrompt == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return systemPrompt;
|
||||
}
|
||||
|
||||
// PUT: api/SystemPrompts/5
|
||||
[HttpPut("{id}")]
|
||||
public async Task<IActionResult> PutSystemPrompt(int id, SystemPrompt systemPrompt)
|
||||
{
|
||||
if (id != systemPrompt.Id)
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
systemPrompt.UpdatedAt = DateTime.UtcNow;
|
||||
_context.Entry(systemPrompt).State = EntityState.Modified;
|
||||
|
||||
try
|
||||
{
|
||||
await _context.SaveChangesAsync();
|
||||
_cache.Remove(CacheKey); // Invalidar caché
|
||||
}
|
||||
catch (DbUpdateConcurrencyException)
|
||||
{
|
||||
if (!SystemPromptExists(id))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
// POST: api/SystemPrompts
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<SystemPrompt>> PostSystemPrompt(SystemPrompt systemPrompt)
|
||||
{
|
||||
systemPrompt.CreatedAt = DateTime.UtcNow;
|
||||
systemPrompt.UpdatedAt = DateTime.UtcNow;
|
||||
_context.SystemPrompts.Add(systemPrompt);
|
||||
await _context.SaveChangesAsync();
|
||||
_cache.Remove(CacheKey); // Invalidar caché
|
||||
|
||||
return CreatedAtAction("GetSystemPrompt", new { id = systemPrompt.Id }, systemPrompt);
|
||||
}
|
||||
|
||||
// DELETE: api/SystemPrompts/5
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<IActionResult> DeleteSystemPrompt(int id)
|
||||
{
|
||||
var systemPrompt = await _context.SystemPrompts.FindAsync(id);
|
||||
if (systemPrompt == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
_context.SystemPrompts.Remove(systemPrompt);
|
||||
await _context.SaveChangesAsync();
|
||||
_cache.Remove(CacheKey); // Invalidar caché
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
// POST: api/SystemPrompts/ToggleActive/5
|
||||
[HttpPost("ToggleActive/{id}")]
|
||||
public async Task<IActionResult> ToggleActive(int id)
|
||||
{
|
||||
var systemPrompt = await _context.SystemPrompts.FindAsync(id);
|
||||
if (systemPrompt == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
systemPrompt.IsActive = !systemPrompt.IsActive;
|
||||
systemPrompt.UpdatedAt = DateTime.UtcNow;
|
||||
await _context.SaveChangesAsync();
|
||||
_cache.Remove(CacheKey); // Invalidar caché
|
||||
|
||||
return Ok(new { IsActive = systemPrompt.IsActive });
|
||||
}
|
||||
|
||||
private bool SystemPromptExists(int id)
|
||||
{
|
||||
return _context.SystemPrompts.Any(e => e.Id == id);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user