diff --git a/backend/Controllers/EquiposController.cs b/backend/Controllers/EquiposController.cs index bb41b47..64db71e 100644 --- a/backend/Controllers/EquiposController.cs +++ b/backend/Controllers/EquiposController.cs @@ -1,8 +1,12 @@ 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; namespace Inventario.API.Controllers { @@ -17,19 +21,18 @@ namespace Inventario.API.Controllers _context = context; } - // --- GET /api/equipos --- - // Consulta todos los equipos con sus relaciones + // --- MÉTODOS CRUD BÁSICOS (Ya implementados) --- + // GET /api/equipos [HttpGet] public async Task Consultar() { - // Query para traer todo en una sola llamada a la BD var query = @" SELECT - e.*, - s.Id as SectorId, s.Nombre as SectorNombre, - u.Id as UsuarioId, u.Username, u.Password, - d.Id as DiscoId, d.Mediatype, d.Size, - mr.Id as MemoriaRamId, mr.part_number as PartNumber, mr.Fabricante, mr.Tamano, mr.Velocidad, emr.Slot + e.Id, e.Hostname, e.Ip, e.Mac, e.Motherboard, e.Cpu, e.Ram_installed, e.Ram_slots, e.Os, e.Architecture, + s.Id as Id, s.Nombre, + u.Id as Id, u.Username, u.Password, + d.Id as Id, d.Mediatype, d.Size, + mr.Id as Id, mr.part_number as PartNumber, mr.Fabricante, mr.Tamano, mr.Velocidad, emr.Slot 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 @@ -61,9 +64,9 @@ namespace Inventario.API.Controllers return equipoActual; }, - splitOn: "SectorId,UsuarioId,DiscoId,MemoriaRamId" + splitOn: "Id,Id,Id,Id" // Dapper divide en cada 'Id' ); - return Ok(equipoDict.Values); + return Ok(equipoDict.Values.OrderBy(e => e.Sector?.Nombre).ThenBy(e => e.Hostname)); } } @@ -201,5 +204,115 @@ namespace Inventario.API.Controllers 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) + SELECT e.id, u.id + FROM dbo.equipos e, dbo.usuarios u + WHERE e.Hostname = @Hostname AND u.Username = @Username;"; + + using (var connection = _context.CreateConnection()) + { + try + { + var filasAfectadas = await connection.ExecuteAsync(query, new { Hostname = hostname, dto.Username }); + if (filasAfectadas == 0) return NotFound("Equipo o usuario no encontrado."); + return Ok(new { success = true }); + } + catch (SqlException ex) when (ex.Number == 2627) // Error de clave primaria duplicada + { + return Conflict("El usuario ya está asociado a este equipo."); + } + } + } + + [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 discos) + { + // La lógica aquí es compleja, la implementaremos en un paso posterior si es necesaria. + // Por ahora, devolvemos un endpoint funcional pero que no hace nada. + Console.WriteLine($"Recibida solicitud para asociar {discos.Count} discos al equipo {hostname}"); + await Task.CompletedTask; // Simula trabajo asíncrono + return Ok(new { message = "Endpoint de asociación de discos recibido, lógica pendiente." }); + } + + [HttpPost("{hostname}/ram")] + public async Task AsociarRam(string hostname, [FromBody] List memorias) + { + // Lógica compleja, pendiente de implementación. + Console.WriteLine($"Recibida solicitud para asociar {memorias.Count} módulos de RAM al equipo {hostname}"); + await Task.CompletedTask; + return Ok(new { message = "Endpoint de asociación de RAM recibido, lógica pendiente." }); + } + + [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); // Timeout de 2 segundos + return Ok(new { isAlive = reply.Status == IPStatus.Success, latency = reply.RoundtripTime }); + } + } + catch (PingException ex) + { + // Maneja errores comunes como "Host desconocido" + return Ok(new { isAlive = false, error = ex.Message }); + } + catch (Exception ex) + { + return StatusCode(500, $"Error interno al hacer ping: {ex.Message}"); + } + } + + // WOL (Wake-On-LAN) es más complejo porque requiere ejecutar comandos de sistema operativo. + // Lo dejamos pendiente para no añadir complejidad de configuración de SSH por ahora. + [HttpPost("wake-on-lan")] + public IActionResult EnviarWol([FromBody] WolRequestDto request) + { + Console.WriteLine($"Recibida solicitud WOL para MAC: {request.Mac}"); + return Ok(new { message = "Solicitud WOL recibida. La ejecución del comando está pendiente de implementación." }); + } } + + // DTOs locales para las peticiones + public class PingRequestDto { public string? Ip { get; set; } } + public class WolRequestDto { public string? Mac { get; set; } } } \ No newline at end of file diff --git a/backend/DTOs/AsociacionUsuarioDto.cs b/backend/DTOs/AsociacionUsuarioDto.cs new file mode 100644 index 0000000..72cbfef --- /dev/null +++ b/backend/DTOs/AsociacionUsuarioDto.cs @@ -0,0 +1,8 @@ +namespace Inventario.API.DTOs +{ + public class AsociacionUsuarioDto + { + // Usamos 'required' para asegurar que este campo nunca sea nulo al recibirlo. + public required string Username { get; set; } + } +} \ No newline at end of file diff --git a/backend/obj/Debug/net9.0/Inventario.API.AssemblyInfo.cs b/backend/obj/Debug/net9.0/Inventario.API.AssemblyInfo.cs index c8eda31..22d619b 100644 --- a/backend/obj/Debug/net9.0/Inventario.API.AssemblyInfo.cs +++ b/backend/obj/Debug/net9.0/Inventario.API.AssemblyInfo.cs @@ -13,7 +13,7 @@ using System.Reflection; [assembly: System.Reflection.AssemblyCompanyAttribute("Inventario.API")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] -[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+10f2f2ba67eac900e8a7b63ced2f3689c309fa6f")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+80210e5d4c8c41b94acb737e8a8d9935a2ef21b6")] [assembly: System.Reflection.AssemblyProductAttribute("Inventario.API")] [assembly: System.Reflection.AssemblyTitleAttribute("Inventario.API")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]