using Dapper; using Inventario.API.Data; using Inventario.API.DTOs; using Inventario.API.Helpers; using Inventario.API.Models; using Microsoft.AspNetCore.Mvc; using System.Data; using System.Net.NetworkInformation; using Microsoft.Data.SqlClient; using Renci.SshNet; using System.Text.RegularExpressions; namespace Inventario.API.Controllers { [ApiController] [Route("api/[controller]")] public class EquiposController : ControllerBase { private readonly DapperContext _context; private readonly IConfiguration _configuration; // 1. Añadimos el campo para la configuración // 2. Modificamos el constructor para inyectar IConfiguration public EquiposController(DapperContext context, IConfiguration configuration) { _context = context; _configuration = configuration; // Asignamos la configuración inyectada } // --- MÉTODOS CRUD BÁSICOS --- // GET /api/equipos [HttpGet] public async Task Consultar() { var query = @" SELECT e.Id, e.Hostname, e.Ip, e.Mac, e.Motherboard, e.Cpu, e.Ram_installed, e.Ram_slots, e.Os, e.Architecture, e.created_at, e.updated_at, e.Origen, e.sector_id, s.Id as Id, s.Nombre, u.Id as Id, u.Username, u.Password, ue.Origen as Origen, d.Id as Id, d.Mediatype, d.Size, ed.Origen as Origen, ed.Id as EquipoDiscoId, mr.Id as Id, mr.part_number as PartNumber, mr.Fabricante, mr.Tamano, mr.Velocidad, emr.Slot, emr.Origen as Origen, emr.Id as EquipoMemoriaRamId FROM dbo.equipos e LEFT JOIN dbo.sectores s ON e.sector_id = s.id LEFT JOIN dbo.usuarios_equipos ue ON e.id = ue.equipo_id LEFT JOIN dbo.usuarios u ON ue.usuario_id = u.id LEFT JOIN dbo.equipos_discos ed ON e.id = ed.equipo_id LEFT JOIN dbo.discos d ON ed.disco_id = d.id LEFT JOIN dbo.equipos_memorias_ram emr ON e.id = emr.equipo_id LEFT JOIN dbo.memorias_ram mr ON emr.memoria_ram_id = mr.id;"; using (var connection = _context.CreateConnection()) { var equipoDict = new Dictionary(); await connection.QueryAsync( query, (equipo, sector, usuario, disco, memoria) => { if (!equipoDict.TryGetValue(equipo.Id, out var equipoActual)) { equipoActual = equipo; equipoActual.Sector = sector; equipoDict.Add(equipoActual.Id, equipoActual); } if (usuario != null && !equipoActual.Usuarios.Any(u => u.Id == usuario.Id)) equipoActual.Usuarios.Add(usuario); if (disco != null && !equipoActual.Discos.Any(d => d.EquipoDiscoId == disco.EquipoDiscoId)) equipoActual.Discos.Add(disco); if (memoria != null && !equipoActual.MemoriasRam.Any(m => m.EquipoMemoriaRamId == memoria.EquipoMemoriaRamId)) equipoActual.MemoriasRam.Add(memoria); return equipoActual; }, splitOn: "Id,Id,Id,Id" ); return Ok(equipoDict.Values.OrderBy(e => e.Sector?.Nombre).ThenBy(e => e.Hostname)); } } // --- GET /api/equipos/{hostname} --- [HttpGet("{hostname}")] public async Task ConsultarDetalle(string hostname) { var query = @"SELECT e.*, s.Id as SectorId, s.Nombre as SectorNombre, u.Id as UsuarioId, u.Username, u.Password, ue.Origen as Origen FROM dbo.equipos e LEFT JOIN dbo.sectores s ON e.sector_id = s.id LEFT JOIN dbo.usuarios_equipos ue ON e.id = ue.equipo_id LEFT JOIN dbo.usuarios u ON ue.usuario_id = u.id WHERE e.Hostname = @Hostname;"; using (var connection = _context.CreateConnection()) { var equipoDict = new Dictionary(); var equipo = (await connection.QueryAsync( query, (e, sector, usuario) => { if (!equipoDict.TryGetValue(e.Id, out var equipoActual)) { equipoActual = e; equipoActual.Sector = sector; equipoDict.Add(equipoActual.Id, equipoActual); } if (usuario != null && !equipoActual.Usuarios.Any(u => u.Id == usuario.Id)) equipoActual.Usuarios.Add(usuario); return equipoActual; }, new { Hostname = hostname }, splitOn: "SectorId,UsuarioId" )).FirstOrDefault(); if (equipo == null) return NotFound("Equipo no encontrado."); var discosQuery = "SELECT d.*, ed.Origen, ed.Id as EquipoDiscoId FROM dbo.discos d JOIN dbo.equipos_discos ed ON d.Id = ed.disco_id WHERE ed.equipo_id = @Id"; equipo.Discos = (await connection.QueryAsync(discosQuery, new { equipo.Id })).ToList(); var ramQuery = "SELECT mr.*, emr.Slot, emr.Origen, emr.Id as EquipoMemoriaRamId FROM dbo.memorias_ram mr JOIN dbo.equipos_memorias_ram emr ON mr.Id = emr.memoria_ram_id WHERE emr.equipo_id = @Id"; equipo.MemoriasRam = (await connection.QueryAsync(ramQuery, new { equipo.Id })).ToList(); return Ok(equipo); } } // --- POST /api/equipos/{hostname} --- [HttpPost("{hostname}")] public async Task Ingresar(string hostname, [FromBody] Equipo equipoData) { var findQuery = "SELECT * FROM dbo.equipos WHERE Hostname = @Hostname;"; using (var connection = _context.CreateConnection()) { var equipoExistente = await connection.QuerySingleOrDefaultAsync(findQuery, new { Hostname = hostname }); if (equipoExistente == null) { // Crear var insertQuery = @"INSERT INTO dbo.equipos (Hostname, Ip, Mac, Motherboard, Cpu, Ram_installed, Ram_slots, Os, Architecture, Origen) VALUES (@Hostname, @Ip, @Mac, @Motherboard, @Cpu, @Ram_installed, @Ram_slots, @Os, @Architecture, 'automatica'); SELECT CAST(SCOPE_IDENTITY() as int);"; var nuevoId = await connection.ExecuteScalarAsync(insertQuery, equipoData); equipoData.Id = nuevoId; return CreatedAtAction(nameof(ConsultarDetalle), new { hostname = equipoData.Hostname }, equipoData); } else { // Actualizar y registrar historial var cambios = new Dictionary(); // Comparamos campos para registrar en historial if (equipoData.Ip != equipoExistente.Ip) cambios["ip"] = (equipoExistente.Ip, equipoData.Ip); if (equipoData.Mac != equipoExistente.Mac) cambios["mac"] = (equipoExistente.Mac ?? "", equipoData.Mac ?? ""); var updateQuery = @"UPDATE dbo.equipos SET Ip = @Ip, Mac = @Mac, Motherboard = @Motherboard, Cpu = @Cpu, Ram_installed = @Ram_installed, Ram_slots = @Ram_slots, Os = @Os, Architecture = @Architecture WHERE Hostname = @Hostname;"; await connection.ExecuteAsync(updateQuery, equipoData); if (cambios.Count > 0) { await HistorialHelper.RegistrarCambios(_context, equipoExistente.Id, cambios); } return Ok(equipoData); } } } // --- PUT /api/equipos/{id} --- [HttpPut("{id}")] public async Task Actualizar(int id, [FromBody] Equipo equipoData) { var updateQuery = @"UPDATE dbo.equipos SET Hostname = @Hostname, Ip = @Ip, Mac = @Mac, Motherboard = @Motherboard, Cpu = @Cpu, Ram_installed = @Ram_installed, Ram_slots = @Ram_slots, Os = @Os, Architecture = @Architecture WHERE Id = @Id;"; using (var connection = _context.CreateConnection()) { // Asignamos el ID del parámetro de la ruta al objeto que recibimos. equipoData.Id = id; // Ahora pasamos el objeto completo a Dapper. var filasAfectadas = await connection.ExecuteAsync(updateQuery, equipoData); if (filasAfectadas == 0) return NotFound("Equipo no encontrado."); return NoContent(); } } // --- DELETE /api/equipos/{id} --- [HttpDelete("{id}")] public async Task Borrar(int id) { var query = "DELETE FROM dbo.equipos WHERE Id = @Id;"; using (var connection = _context.CreateConnection()) { var filasAfectadas = await connection.ExecuteAsync(query, new { Id = id }); if (filasAfectadas == 0) { return NotFound("Equipo no encontrado."); } return NoContent(); } } // --- GET /api/equipos/{hostname}/historial --- [HttpGet("{hostname}/historial")] public async Task ConsultarHistorial(string hostname) { var query = @"SELECT h.* FROM dbo.historial_equipos h JOIN dbo.equipos e ON h.equipo_id = e.id WHERE e.Hostname = @Hostname ORDER BY h.fecha_cambio DESC;"; using (var connection = _context.CreateConnection()) { var equipo = await connection.QueryFirstOrDefaultAsync("SELECT Id FROM dbo.equipos WHERE Hostname = @Hostname", new { Hostname = hostname }); if (equipo == null) return NotFound("Equipo no encontrado."); var historial = await connection.QueryAsync(query, new { Hostname = hostname }); return Ok(new { equipo = hostname, historial }); } } // --- MÉTODOS DE ASOCIACIÓN Y COMANDOS --- [HttpPatch("{id_equipo}/sector/{id_sector}")] public async Task AsociarSector(int id_equipo, int id_sector) { var query = "UPDATE dbo.equipos SET sector_id = @IdSector WHERE Id = @IdEquipo;"; using (var connection = _context.CreateConnection()) { var filasAfectadas = await connection.ExecuteAsync(query, new { IdSector = id_sector, IdEquipo = id_equipo }); if (filasAfectadas == 0) return NotFound("Equipo o sector no encontrado."); return Ok(new { success = true }); // Devolvemos una respuesta simple de éxito } } [HttpPost("{hostname}/asociarusuario")] public async Task AsociarUsuario(string hostname, [FromBody] AsociacionUsuarioDto dto) { var query = @" INSERT INTO dbo.usuarios_equipos (equipo_id, usuario_id, origen) SELECT e.id, u.id, 'automatica' FROM dbo.equipos e, dbo.usuarios u WHERE e.Hostname = @Hostname AND u.Username = @Username AND NOT EXISTS ( SELECT 1 FROM dbo.usuarios_equipos ue WHERE ue.equipo_id = e.id AND ue.usuario_id = u.id );"; using (var connection = _context.CreateConnection()) { await connection.ExecuteAsync(query, new { Hostname = hostname, dto.Username }); return Ok(new { success = true, message = "Asociación asegurada." }); } } [HttpDelete("{hostname}/usuarios/{username}")] public async Task DesasociarUsuario(string hostname, string username) { var query = @" DELETE FROM dbo.usuarios_equipos WHERE equipo_id = (SELECT id FROM dbo.equipos WHERE Hostname = @Hostname) AND usuario_id = (SELECT id FROM dbo.usuarios WHERE Username = @Username);"; using (var connection = _context.CreateConnection()) { var filasAfectadas = await connection.ExecuteAsync(query, new { Hostname = hostname, Username = username }); if (filasAfectadas == 0) return NotFound("Asociación no encontrada."); return Ok(new { success = true }); } } [HttpPost("{hostname}/asociardiscos")] public async Task AsociarDiscos(string hostname, [FromBody] List discosDesdeCliente) { var equipoQuery = "SELECT * FROM dbo.equipos WHERE Hostname = @Hostname;"; using var connection = _context.CreateConnection(); connection.Open(); var equipo = await connection.QuerySingleOrDefaultAsync(equipoQuery, new { Hostname = hostname }); if (equipo == null) { return NotFound("Equipo no encontrado."); } using var transaction = connection.BeginTransaction(); try { var discosActualesQuery = @" SELECT d.Id, d.Mediatype, d.Size, ed.Id as EquipoDiscoId FROM dbo.equipos_discos ed JOIN dbo.discos d ON ed.disco_id = d.id WHERE ed.equipo_id = @EquipoId;"; var discosEnDb = (await connection.QueryAsync(discosActualesQuery, new { EquipoId = equipo.Id }, transaction)).ToList(); var discosClienteContados = discosDesdeCliente .GroupBy(d => $"{d.Mediatype}_{d.Size}") .ToDictionary(g => g.Key, g => g.Count()); var discosDbContados = discosEnDb .GroupBy(d => $"{d.Mediatype}_{d.Size}") .ToDictionary(g => g.Key, g => g.Count()); var cambios = new Dictionary(); var discosAEliminar = new List(); foreach (var discoDb in discosEnDb) { var key = $"{discoDb.Mediatype}_{discoDb.Size}"; if (discosClienteContados.TryGetValue(key, out int count) && count > 0) { discosClienteContados[key]--; } else { discosAEliminar.Add(discoDb.EquipoDiscoId); var nombreDisco = $"Disco {discoDb.Mediatype} {discoDb.Size}GB"; var anterior = discosDbContados.GetValueOrDefault(key, 0); if (!cambios.ContainsKey(nombreDisco)) cambios[nombreDisco] = (anterior.ToString(), (anterior - 1).ToString()); else cambios[nombreDisco] = (anterior.ToString(), (int.Parse(cambios[nombreDisco].nuevo!) - 1).ToString()); } } if (discosAEliminar.Any()) { await connection.ExecuteAsync("DELETE FROM dbo.equipos_discos WHERE Id IN @Ids;", new { Ids = discosAEliminar }, transaction); } foreach (var discoCliente in discosDesdeCliente) { var key = $"{discoCliente.Mediatype}_{discoCliente.Size}"; if (discosDbContados.TryGetValue(key, out int count) && count > 0) { discosDbContados[key]--; } else { var disco = await connection.QueryFirstOrDefaultAsync("SELECT * FROM dbo.discos WHERE Mediatype = @Mediatype AND Size = @Size;", discoCliente, transaction); if (disco == null) continue; await connection.ExecuteAsync("INSERT INTO dbo.equipos_discos (equipo_id, disco_id, origen) VALUES (@EquipoId, @DiscoId, 'automatica');", new { EquipoId = equipo.Id, DiscoId = disco.Id }, transaction); var nombreDisco = $"Disco {disco.Mediatype} {disco.Size}GB"; var anterior = discosDbContados.GetValueOrDefault(key, 0); if (!cambios.ContainsKey(nombreDisco)) cambios[nombreDisco] = (anterior.ToString(), (anterior + 1).ToString()); else cambios[nombreDisco] = (anterior.ToString(), (int.Parse(cambios[nombreDisco].nuevo!) + 1).ToString()); } } if (cambios.Count > 0) { var cambiosFormateados = cambios.ToDictionary( kvp => kvp.Key, kvp => ((string?)$"{kvp.Value.anterior} Instalados", (string?)$"{kvp.Value.nuevo} Instalados") ); await HistorialHelper.RegistrarCambios(_context, equipo.Id, cambiosFormateados); } transaction.Commit(); return Ok(new { message = "Discos sincronizados correctamente." }); } catch (Exception ex) { transaction.Rollback(); Console.WriteLine($"Error al asociar discos para {hostname}: {ex.Message}"); return StatusCode(500, "Ocurrió un error interno al procesar la solicitud."); } } [HttpPost("{hostname}/ram")] public async Task AsociarRam(string hostname, [FromBody] List memoriasDesdeCliente) { var equipoQuery = "SELECT * FROM dbo.equipos WHERE Hostname = @Hostname;"; using var connection = _context.CreateConnection(); connection.Open(); var equipo = await connection.QuerySingleOrDefaultAsync(equipoQuery, new { Hostname = hostname }); if (equipo == null) return NotFound("Equipo no encontrado."); using var transaction = connection.BeginTransaction(); try { var ramActualQuery = @" SELECT emr.Id as EquipoMemoriaRamId, emr.Slot, mr.Id, mr.part_number as PartNumber, mr.Fabricante, mr.Tamano, mr.Velocidad FROM dbo.equipos_memorias_ram emr JOIN dbo.memorias_ram mr ON emr.memoria_ram_id = mr.id WHERE emr.equipo_id = @EquipoId;"; var ramEnDb = (await connection.QueryAsync(ramActualQuery, new { EquipoId = equipo.Id }, transaction)).ToList(); Func crearHuella = ram => $"{ram.Slot}_{ram.PartNumber ?? ""}_{ram.Tamano}_{ram.Velocidad ?? 0}"; var huellasCliente = new HashSet(memoriasDesdeCliente.Select(crearHuella)); var huellasDb = new HashSet(ramEnDb.Select(crearHuella)); var cambios = new Dictionary(); Func formatRamDetails = ram => { var parts = new List { ram.Fabricante, $"{ram.Tamano}GB", ram.PartNumber, ram.Velocidad?.ToString() + "MHz" }; return string.Join(" ", parts.Where(p => !string.IsNullOrEmpty(p))); }; var modulosEliminados = ramEnDb.Where(ramDb => !huellasCliente.Contains(crearHuella(ramDb))).ToList(); foreach (var modulo in modulosEliminados) { var campo = $"RAM Slot {modulo.Slot}"; cambios[campo] = (formatRamDetails(modulo), "Vacio"); } var modulosInsertados = memoriasDesdeCliente.Where(ramCliente => !huellasDb.Contains(crearHuella(ramCliente))).ToList(); foreach (var modulo in modulosInsertados) { var campo = $"RAM Slot {modulo.Slot}"; var valorNuevo = formatRamDetails(modulo); if (cambios.ContainsKey(campo)) { cambios[campo] = (cambios[campo].anterior, valorNuevo); } else { cambios[campo] = ("Vacio", valorNuevo); } } var asociacionesAEliminar = modulosEliminados.Select(ramDb => (int)ramDb.EquipoMemoriaRamId).ToList(); if (asociacionesAEliminar.Any()) { await connection.ExecuteAsync("DELETE FROM dbo.equipos_memorias_ram WHERE Id IN @Ids;", new { Ids = asociacionesAEliminar }, transaction); } foreach (var memInfo in modulosInsertados) { var findRamQuery = @"SELECT * FROM dbo.memorias_ram WHERE (part_number = @PartNumber OR (part_number IS NULL AND @PartNumber IS NULL)) AND tamano = @Tamano AND (velocidad = @Velocidad OR (velocidad IS NULL AND @Velocidad IS NULL));"; var memoriaMaestra = await connection.QuerySingleOrDefaultAsync(findRamQuery, memInfo, transaction); int memoriaMaestraId; if (memoriaMaestra == null) { var insertRamQuery = @"INSERT INTO dbo.memorias_ram (part_number, fabricante, tamano, velocidad) VALUES (@PartNumber, @Fabricante, @Tamano, @Velocidad); SELECT CAST(SCOPE_IDENTITY() as int);"; memoriaMaestraId = await connection.ExecuteScalarAsync(insertRamQuery, memInfo, transaction); } else { memoriaMaestraId = memoriaMaestra.Id; } // Crear la asociación en la tabla intermedia var insertAsociacionQuery = "INSERT INTO dbo.equipos_memorias_ram (equipo_id, memoria_ram_id, slot, origen) VALUES (@EquipoId, @MemoriaRamId, @Slot, 'automatica');"; await connection.ExecuteAsync(insertAsociacionQuery, new { EquipoId = equipo.Id, MemoriaRamId = memoriaMaestraId, memInfo.Slot }, transaction); } if (cambios.Count > 0) { await HistorialHelper.RegistrarCambios(_context, equipo.Id, cambios); } transaction.Commit(); return Ok(new { message = "Módulos de RAM sincronizados correctamente." }); } catch (Exception ex) { transaction.Rollback(); Console.WriteLine($"Error al asociar RAM para {hostname}: {ex.Message}"); return StatusCode(500, "Ocurrió un error interno al procesar la solicitud de RAM."); } } [HttpPost("ping")] public async Task EnviarPing([FromBody] PingRequestDto request) { if (string.IsNullOrWhiteSpace(request.Ip)) return BadRequest("La dirección IP es requerida."); try { using (var ping = new Ping()) { var reply = await ping.SendPingAsync(request.Ip, 2000); bool isAlive = reply.Status == IPStatus.Success; if (!isAlive) { reply = await ping.SendPingAsync(request.Ip, 2000); isAlive = reply.Status == IPStatus.Success; } return Ok(new { isAlive, latency = isAlive ? reply.RoundtripTime : (long?)null }); } } catch (PingException ex) { Console.WriteLine($"Error de Ping para {request.Ip}: {ex.Message}"); return Ok(new { isAlive = false, error = "Host no alcanzable o desconocido." }); } catch (Exception ex) { Console.WriteLine($"Error interno al hacer ping a {request.Ip}: {ex.Message}"); return StatusCode(500, "Error interno del servidor al realizar el ping."); } } [HttpPost("wake-on-lan")] public IActionResult EnviarWol([FromBody] WolRequestDto request) { var mac = request.Mac; var ip = request.Ip; if (string.IsNullOrWhiteSpace(mac) || !Regex.IsMatch(mac, "^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$")) { return BadRequest("Formato de dirección MAC inválido."); } if (string.IsNullOrWhiteSpace(ip) || !Regex.IsMatch(ip, @"^(\d{1,3}\.){3}\d{1,3}$")) { return BadRequest("Formato de dirección IP inválido."); } var octetos = ip.Split('.'); if (octetos.Length != 4) { return BadRequest("Formato de dirección IP incorrecto."); } var vlanNumber = octetos[2]; var interfaceName = $"vlan{vlanNumber}"; // 3. Leemos los valores desde la configuración en lugar de hardcodearlos var sshHost = _configuration.GetValue("SshSettings:Host"); var sshPort = _configuration.GetValue("SshSettings:Port"); var sshUser = _configuration.GetValue("SshSettings:User"); var sshPass = _configuration.GetValue("SshSettings:Password"); if (string.IsNullOrEmpty(sshHost) || string.IsNullOrEmpty(sshUser) || string.IsNullOrEmpty(sshPass)) { Console.WriteLine("Error: La configuración SSH no está completa en appsettings.json."); return StatusCode(500, "La configuración del servidor SSH está incompleta."); } try { using (var client = new SshClient(sshHost, sshPort, sshUser, sshPass)) { client.Connect(); if (client.IsConnected) { var command = $"/usr/sbin/etherwake -b -i {interfaceName} {mac}"; var sshCommand = client.CreateCommand(command); sshCommand.Execute(); Console.WriteLine($"Comando WOL ejecutado: {sshCommand.CommandText}"); client.Disconnect(); } else { Console.WriteLine("Error: No se pudo conectar al servidor SSH."); } } } catch (Exception ex) { Console.WriteLine($"Error al ejecutar comando WOL: {ex.Message}"); } return NoContent(); } [HttpPost("manual")] public async Task CrearEquipoManual([FromBody] CrearEquipoManualDto equipoDto) { var findQuery = "SELECT Id FROM dbo.equipos WHERE Hostname = @Hostname;"; var insertQuery = @" INSERT INTO dbo.equipos (Hostname, Ip, Motherboard, Cpu, Os, Sector_id, Origen, Ram_installed, Architecture) VALUES (@Hostname, @Ip, @Motherboard, @Cpu, @Os, @Sector_id, 'manual', 0, ''); SELECT CAST(SCOPE_IDENTITY() as int);"; using (var connection = _context.CreateConnection()) { var existente = await connection.QueryFirstOrDefaultAsync(findQuery, new { equipoDto.Hostname }); if (existente.HasValue) { return Conflict($"El hostname '{equipoDto.Hostname}' ya existe."); } var nuevoId = await connection.ExecuteScalarAsync(insertQuery, equipoDto); await HistorialHelper.RegistrarCambioUnico(_context, nuevoId, "Equipo", null, "Equipo creado manualmente"); var nuevoEquipo = await connection.QuerySingleOrDefaultAsync("SELECT * FROM dbo.equipos WHERE Id = @Id", new { Id = nuevoId }); if (nuevoEquipo == null) { return StatusCode(500, "No se pudo recuperar el equipo después de crearlo."); } return CreatedAtAction(nameof(ConsultarDetalle), new { hostname = nuevoEquipo.Hostname }, nuevoEquipo); } } // --- ENDPOINTS PARA BORRADO MANUAL DE ASOCIACIONES --- [HttpDelete("asociacion/disco/{equipoDiscoId}")] public async Task BorrarAsociacionDisco(int equipoDiscoId) { using (var connection = _context.CreateConnection()) { var infoQuery = @" SELECT ed.equipo_id, d.Mediatype, d.Size FROM dbo.equipos_discos ed JOIN dbo.discos d ON ed.disco_id = d.id WHERE ed.Id = @EquipoDiscoId AND ed.Origen = 'manual'"; var info = await connection.QueryFirstOrDefaultAsync<(int equipo_id, string Mediatype, int Size)>(infoQuery, new { EquipoDiscoId = equipoDiscoId }); if (info == default) { return NotFound("Asociación de disco no encontrada o no es manual."); } var deleteQuery = "DELETE FROM dbo.equipos_discos WHERE Id = @EquipoDiscoId;"; await connection.ExecuteAsync(deleteQuery, new { EquipoDiscoId = equipoDiscoId }); var descripcion = $"Disco {info.Mediatype} {info.Size}GB"; await HistorialHelper.RegistrarCambioUnico(_context, info.equipo_id, "Componente", descripcion, "Eliminado"); await connection.ExecuteAsync("UPDATE dbo.equipos SET updated_at = GETDATE() WHERE Id = @Id", new { Id = info.equipo_id }); return NoContent(); } } [HttpDelete("asociacion/ram/{equipoMemoriaRamId}")] public async Task BorrarAsociacionRam(int equipoMemoriaRamId) { using (var connection = _context.CreateConnection()) { var infoQuery = @" SELECT emr.equipo_id, emr.Slot, mr.Fabricante, mr.Tamano, mr.Velocidad FROM dbo.equipos_memorias_ram emr JOIN dbo.memorias_ram mr ON emr.memoria_ram_id = mr.id WHERE emr.Id = @Id AND emr.Origen = 'manual'"; var info = await connection.QueryFirstOrDefaultAsync<(int equipo_id, string Slot, string? Fabricante, int Tamano, int? Velocidad)>(infoQuery, new { Id = equipoMemoriaRamId }); if (info == default) { return NotFound("Asociación de RAM no encontrada o no es manual."); } var deleteQuery = "DELETE FROM dbo.equipos_memorias_ram WHERE Id = @EquipoMemoriaRamId;"; await connection.ExecuteAsync(deleteQuery, new { EquipoMemoriaRamId = equipoMemoriaRamId }); var descripcion = $"Módulo RAM: Slot {info.Slot} - {info.Fabricante ?? ""} {info.Tamano}GB {info.Velocidad?.ToString() ?? ""}MHz"; await HistorialHelper.RegistrarCambioUnico(_context, info.equipo_id, "Componente", descripcion, "Eliminado"); var updateQuery = @" UPDATE e SET e.ram_installed = ISNULL((SELECT SUM(mr.Tamano) FROM dbo.equipos_memorias_ram emr JOIN dbo.memorias_ram mr ON emr.memoria_ram_id = mr.id WHERE emr.equipo_id = @Id), 0), e.updated_at = GETDATE() FROM dbo.equipos e WHERE e.Id = @Id;"; await connection.ExecuteAsync(updateQuery, new { Id = info.equipo_id }); return NoContent(); } } [HttpDelete("asociacion/usuario/{equipoId}/{usuarioId}")] public async Task BorrarAsociacionUsuario(int equipoId, int usuarioId) { using (var connection = _context.CreateConnection()) { var username = await connection.QuerySingleOrDefaultAsync("SELECT Username FROM dbo.usuarios WHERE Id = @UsuarioId", new { UsuarioId = usuarioId }); if (username == null) { return NotFound("Usuario no encontrado."); } var deleteQuery = "DELETE FROM dbo.usuarios_equipos WHERE equipo_id = @EquipoId AND usuario_id = @UsuarioId AND Origen = 'manual';"; var filasAfectadas = await connection.ExecuteAsync(deleteQuery, new { EquipoId = equipoId, UsuarioId = usuarioId }); if (filasAfectadas == 0) { return NotFound("Asociación de usuario no encontrada o no es manual."); } var descripcion = $"Usuario {username}"; await HistorialHelper.RegistrarCambioUnico(_context, equipoId, "Componente", descripcion, "Eliminado"); await connection.ExecuteAsync("UPDATE dbo.equipos SET updated_at = GETDATE() WHERE Id = @Id", new { Id = equipoId }); return NoContent(); } } [HttpPut("manual/{id}")] public async Task ActualizarEquipoManual(int id, [FromBody] EditarEquipoManualDto equipoDto) { using (var connection = _context.CreateConnection()) { var equipoActual = await connection.QuerySingleOrDefaultAsync("SELECT * FROM dbo.equipos WHERE Id = @Id", new { Id = id }); if (equipoActual == null) { return NotFound("El equipo no existe."); } if (equipoActual.Origen != "manual") { return Forbid("No se puede modificar un equipo generado automáticamente."); } if (equipoActual.Hostname != equipoDto.Hostname) { var hostExistente = await connection.QuerySingleOrDefaultAsync("SELECT Id FROM dbo.equipos WHERE Hostname = @Hostname AND Id != @Id", new { equipoDto.Hostname, Id = id }); if (hostExistente.HasValue) { return Conflict($"El hostname '{equipoDto.Hostname}' ya está en uso por otro equipo."); } } var allSectores = await connection.QueryAsync("SELECT Id, Nombre FROM dbo.sectores;"); var sectorMap = allSectores.ToDictionary(s => s.Id, s => s.Nombre); var cambios = new Dictionary(); if (equipoActual.Hostname != equipoDto.Hostname) cambios["Hostname"] = (equipoActual.Hostname, equipoDto.Hostname); if (equipoActual.Ip != equipoDto.Ip) cambios["IP"] = (equipoActual.Ip, equipoDto.Ip); if (equipoActual.Mac != equipoDto.Mac) cambios["MAC Address"] = (equipoActual.Mac ?? "N/A", equipoDto.Mac ?? "N/A"); if (equipoActual.Motherboard != equipoDto.Motherboard) cambios["Motherboard"] = (equipoActual.Motherboard ?? "N/A", equipoDto.Motherboard ?? "N/A"); if (equipoActual.Cpu != equipoDto.Cpu) cambios["CPU"] = (equipoActual.Cpu ?? "N/A", equipoDto.Cpu ?? "N/A"); if (equipoActual.Os != equipoDto.Os) cambios["Sistema Operativo"] = (equipoActual.Os ?? "N/A", equipoDto.Os ?? "N/A"); if (equipoActual.Architecture != equipoDto.Architecture) cambios["Arquitectura"] = (equipoActual.Architecture ?? "N/A", equipoDto.Architecture ?? "N/A"); if (equipoActual.Ram_slots != equipoDto.Ram_slots) cambios["Slots RAM"] = (equipoActual.Ram_slots?.ToString() ?? "N/A", equipoDto.Ram_slots?.ToString() ?? "N/A"); if (equipoActual.Sector_id != equipoDto.Sector_id) { string nombreAnterior = equipoActual.Sector_id.HasValue && sectorMap.TryGetValue(equipoActual.Sector_id.Value, out var oldName) ? oldName : "Ninguno"; string nombreNuevo = equipoDto.Sector_id.HasValue && sectorMap.TryGetValue(equipoDto.Sector_id.Value, out var newName) ? newName : "Ninguno"; cambios["Sector"] = (nombreAnterior, nombreNuevo); } var updateQuery = @"UPDATE dbo.equipos SET Hostname = @Hostname, Ip = @Ip, Mac = @Mac, Motherboard = @Motherboard, Cpu = @Cpu, Os = @Os, Sector_id = @Sector_id, Ram_slots = @Ram_slots, Architecture = @Architecture, -- Campo añadido a la actualización updated_at = GETDATE() OUTPUT INSERTED.* WHERE Id = @Id AND Origen = 'manual';"; var equipoActualizado = await connection.QuerySingleOrDefaultAsync(updateQuery, new { equipoDto.Hostname, equipoDto.Ip, mac = equipoDto.Mac, equipoDto.Motherboard, equipoDto.Cpu, equipoDto.Os, equipoDto.Sector_id, equipoDto.Ram_slots, equipoDto.Architecture, Id = id }); if (equipoActualizado == null) { return StatusCode(500, "No se pudo actualizar el equipo."); } if (cambios.Count > 0) { await HistorialHelper.RegistrarCambios(_context, id, cambios); } var equipoCompleto = await ConsultarDetalle(equipoActualizado.Hostname); return equipoCompleto; } } [HttpPost("manual/{equipoId}/disco")] public async Task AsociarDiscoManual(int equipoId, [FromBody] AsociarDiscoManualDto dto) { using (var connection = _context.CreateConnection()) { var equipo = await connection.QueryFirstOrDefaultAsync("SELECT Origen FROM dbo.equipos WHERE Id = @Id", new { Id = equipoId }); if (equipo == null || equipo.Origen != "manual") return Forbid("Solo se pueden añadir componentes a equipos manuales."); var discoMaestro = await connection.QueryFirstOrDefaultAsync("SELECT * FROM dbo.discos WHERE Mediatype = @Mediatype AND Size = @Size", dto); int discoId; if (discoMaestro == null) { discoId = await connection.ExecuteScalarAsync("INSERT INTO dbo.discos (Mediatype, Size) VALUES (@Mediatype, @Size); SELECT CAST(SCOPE_IDENTITY() as int);", dto); } else { discoId = discoMaestro.Id; } var asociacionQuery = "INSERT INTO dbo.equipos_discos (equipo_id, disco_id, origen) VALUES (@EquipoId, @DiscoId, 'manual'); SELECT CAST(SCOPE_IDENTITY() as int);"; var nuevaAsociacionId = await connection.ExecuteScalarAsync(asociacionQuery, new { EquipoId = equipoId, DiscoId = discoId }); var descripcion = $"Disco {dto.Mediatype} {dto.Size}GB"; await HistorialHelper.RegistrarCambioUnico(_context, equipoId, "Componente", null, $"Añadido: {descripcion}"); await connection.ExecuteAsync("UPDATE dbo.equipos SET updated_at = GETDATE() WHERE Id = @Id", new { Id = equipoId }); return Ok(new { message = "Disco asociado manualmente.", equipoDiscoId = nuevaAsociacionId }); } } [HttpPost("manual/{equipoId}/ram")] public async Task AsociarRamManual(int equipoId, [FromBody] AsociarRamManualDto dto) { using (var connection = _context.CreateConnection()) { var equipo = await connection.QueryFirstOrDefaultAsync("SELECT Origen FROM dbo.equipos WHERE Id = @Id", new { Id = equipoId }); if (equipo == null || equipo.Origen != "manual") return Forbid("Solo se pueden añadir componentes a equipos manuales."); int ramId; var ramMaestra = await connection.QueryFirstOrDefaultAsync( "SELECT * FROM dbo.memorias_ram WHERE (Fabricante = @Fabricante OR (Fabricante IS NULL AND @Fabricante IS NULL)) AND Tamano = @Tamano AND (Velocidad = @Velocidad OR (Velocidad IS NULL AND @Velocidad IS NULL))", dto); if (ramMaestra == null) { var insertQuery = @"INSERT INTO dbo.memorias_ram (part_number, fabricante, tamano, velocidad) VALUES (@PartNumber, @Fabricante, @Tamano, @Velocidad); SELECT CAST(SCOPE_IDENTITY() as int);"; ramId = await connection.ExecuteScalarAsync(insertQuery, dto); } else { ramId = ramMaestra.Id; } var asociacionQuery = "INSERT INTO dbo.equipos_memorias_ram (equipo_id, memoria_ram_id, slot, origen) VALUES (@EquipoId, @RamId, @Slot, 'manual'); SELECT CAST(SCOPE_IDENTITY() as int);"; var nuevaAsociacionId = await connection.ExecuteScalarAsync(asociacionQuery, new { EquipoId = equipoId, RamId = ramId, dto.Slot }); var descripcion = $"Módulo RAM: Slot {dto.Slot} - {dto.Fabricante ?? ""} {dto.Tamano}GB {dto.Velocidad?.ToString() ?? ""}MHz"; await HistorialHelper.RegistrarCambioUnico(_context, equipoId, "Componente", null, $"Añadido: {descripcion}"); var updateQuery = @" UPDATE e SET e.ram_installed = ISNULL((SELECT SUM(mr.Tamano) FROM dbo.equipos_memorias_ram emr JOIN dbo.memorias_ram mr ON emr.memoria_ram_id = mr.id WHERE emr.equipo_id = @Id), 0), e.updated_at = GETDATE() FROM dbo.equipos e WHERE e.Id = @Id;"; await connection.ExecuteAsync(updateQuery, new { Id = equipoId }); return Ok(new { message = "RAM asociada manualmente.", equipoMemoriaRamId = nuevaAsociacionId }); } } [HttpPost("manual/{equipoId}/usuario")] public async Task AsociarUsuarioManual(int equipoId, [FromBody] AsociarUsuarioManualDto dto) { using (var connection = _context.CreateConnection()) { var equipo = await connection.QueryFirstOrDefaultAsync("SELECT Origen FROM dbo.equipos WHERE Id = @Id", new { Id = equipoId }); if (equipo == null || equipo.Origen != "manual") return Forbid("Solo se pueden añadir componentes a equipos manuales."); int usuarioId; var usuario = await connection.QueryFirstOrDefaultAsync("SELECT * FROM dbo.usuarios WHERE Username = @Username", dto); if (usuario == null) { usuarioId = await connection.ExecuteScalarAsync("INSERT INTO dbo.usuarios (Username) VALUES (@Username); SELECT CAST(SCOPE_IDENTITY() as int);", dto); } else { usuarioId = usuario.Id; } try { var asociacionQuery = "INSERT INTO dbo.usuarios_equipos (equipo_id, usuario_id, origen) VALUES (@EquipoId, @UsuarioId, 'manual');"; await connection.ExecuteAsync(asociacionQuery, new { EquipoId = equipoId, UsuarioId = usuarioId }); } catch (SqlException ex) when (ex.Number == 2627) { return Conflict("El usuario ya está asociado a este equipo."); } var descripcion = $"Usuario {dto.Username}"; await HistorialHelper.RegistrarCambioUnico(_context, equipoId, "Componente", null, $"Añadido: {descripcion}"); await connection.ExecuteAsync("UPDATE dbo.equipos SET updated_at = GETDATE() WHERE Id = @Id", new { Id = equipoId }); return Ok(new { message = "Usuario asociado manualmente." }); } } [HttpGet("distinct/{fieldName}")] public async Task GetDistinctFieldValues(string fieldName) { // 1. Lista blanca de campos permitidos para evitar inyección SQL y exposición de datos. var allowedFields = new List { "os", "cpu", "motherboard", "architecture" }; if (!allowedFields.Contains(fieldName.ToLower())) { return BadRequest("El campo especificado no es válido o no está permitido."); } // 2. Construir la consulta de forma segura var query = $"SELECT DISTINCT {fieldName} FROM dbo.equipos WHERE {fieldName} IS NOT NULL AND {fieldName} != '' ORDER BY {fieldName};"; using (var connection = _context.CreateConnection()) { var values = await connection.QueryAsync(query); return Ok(values); } } // DTOs locales para las peticiones public class PingRequestDto { public string? Ip { get; set; } } public class WolRequestDto { public string? Mac { get; set; } public string? Ip { get; set; } } class DiscoAsociado { public int Id { get; set; } public string Mediatype { get; set; } = ""; public int Size { get; set; } public int EquipoDiscoId { get; set; } } public class CrearEquipoManualDto { public required string Hostname { get; set; } public required string Ip { get; set; } public string? Motherboard { get; set; } public string? Cpu { get; set; } public string? Os { get; set; } public int? Sector_id { get; set; } } public class EditarEquipoManualDto { public required string Hostname { get; set; } public required string Ip { get; set; } public string? Mac { get; set; } public string? Motherboard { get; set; } public string? Cpu { get; set; } public string? Os { get; set; } public int? Sector_id { get; set; } public int? Ram_slots { get; set; } public string? Architecture { get; set; } } public class AsociarDiscoManualDto { public required string Mediatype { get; set; } public int Size { get; set; } } public class AsociarRamManualDto { public required string Slot { get; set; } public int Tamano { get; set; } public string? Fabricante { get; set; } public int? Velocidad { get; set; } public string? PartNumber { get; set; } } public class AsociarUsuarioManualDto { public required string Username { get; set; } } } }