430 lines
18 KiB
C#
430 lines
18 KiB
C#
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
|
|
{
|
|
[ApiController]
|
|
[Route("api/[controller]")]
|
|
public class EquiposController : ControllerBase
|
|
{
|
|
private readonly DapperContext _context;
|
|
|
|
public EquiposController(DapperContext context)
|
|
{
|
|
_context = context;
|
|
}
|
|
|
|
// --- MÉTODOS CRUD BÁSICOS (Ya implementados) ---
|
|
// GET /api/equipos
|
|
[HttpGet]
|
|
public async Task<IActionResult> 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,
|
|
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
|
|
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<int, Equipo>();
|
|
|
|
await connection.QueryAsync<Equipo, Sector, Usuario, Disco, MemoriaRamDetalle, Equipo>(
|
|
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.Id == disco.Id))
|
|
equipoActual.Discos.Add(disco);
|
|
if (memoria != null && !equipoActual.MemoriasRam.Any(m => m.Id == memoria.Id && m.Slot == memoria.Slot))
|
|
equipoActual.MemoriasRam.Add(memoria);
|
|
|
|
return equipoActual;
|
|
},
|
|
splitOn: "Id,Id,Id,Id" // Dapper divide en cada 'Id'
|
|
);
|
|
return Ok(equipoDict.Values.OrderBy(e => e.Sector?.Nombre).ThenBy(e => e.Hostname));
|
|
}
|
|
}
|
|
|
|
// --- GET /api/equipos/{hostname} ---
|
|
[HttpGet("{hostname}")]
|
|
public async Task<IActionResult> ConsultarDetalle(string hostname)
|
|
{
|
|
var query = @"SELECT
|
|
e.*,
|
|
s.Id as SectorId, s.Nombre as SectorNombre,
|
|
u.Id as UsuarioId, u.Username, u.Password
|
|
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<int, Equipo>();
|
|
var equipo = (await connection.QueryAsync<Equipo, Sector, Usuario, Equipo>(
|
|
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.");
|
|
|
|
return Ok(equipo);
|
|
}
|
|
}
|
|
|
|
// --- POST /api/equipos/{hostname} ---
|
|
[HttpPost("{hostname}")]
|
|
public async Task<IActionResult> 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<Equipo>(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)
|
|
VALUES (@Hostname, @Ip, @Mac, @Motherboard, @Cpu, @Ram_installed, @Ram_slots, @Os, @Architecture);
|
|
SELECT CAST(SCOPE_IDENTITY() as int);";
|
|
var nuevoId = await connection.ExecuteScalarAsync<int>(insertQuery, equipoData);
|
|
equipoData.Id = nuevoId;
|
|
return CreatedAtAction(nameof(ConsultarDetalle), new { hostname = equipoData.Hostname }, equipoData);
|
|
}
|
|
else
|
|
{
|
|
// Actualizar y registrar historial
|
|
var cambios = new Dictionary<string, (string anterior, string nuevo)>();
|
|
|
|
// 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<IActionResult> 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<IActionResult> Borrar(int id)
|
|
{
|
|
// La base de datos está configurada con ON DELETE CASCADE, por lo que las relaciones se borrarán automáticamente.
|
|
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<IActionResult> 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<Equipo>("SELECT Id FROM dbo.equipos WHERE Hostname = @Hostname", new { Hostname = hostname });
|
|
if (equipo == null) return NotFound("Equipo no encontrado.");
|
|
|
|
var historial = await connection.QueryAsync<HistorialEquipo>(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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> AsociarDiscos(string hostname, [FromBody] List<Disco> discosDesdeCliente)
|
|
{
|
|
// 1. OBTENER EL EQUIPO
|
|
var equipoQuery = "SELECT * FROM dbo.equipos WHERE Hostname = @Hostname;";
|
|
using var connection = _context.CreateConnection();
|
|
connection.Open();
|
|
var equipo = await connection.QuerySingleOrDefaultAsync<Equipo>(equipoQuery, new { Hostname = hostname });
|
|
|
|
if (equipo == null)
|
|
{
|
|
return NotFound("Equipo no encontrado.");
|
|
}
|
|
|
|
// Iniciar una transacción para asegurar que todas las operaciones se completen o ninguna lo haga.
|
|
using var transaction = connection.BeginTransaction();
|
|
try
|
|
{
|
|
// 2. OBTENER ASOCIACIONES Y DISCOS ACTUALES DE LA BD
|
|
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;";
|
|
|
|
// Creamos una clase anónima temporal para mapear el resultado del JOIN
|
|
var discosEnDb = (await connection.QueryAsync(discosActualesQuery, new { EquipoId = equipo.Id }, transaction)).Select(d => new
|
|
{
|
|
Id = (int)d.Id,
|
|
Mediatype = (string)d.Mediatype,
|
|
Size = (int)d.Size,
|
|
EquipoDiscoId = (int)d.EquipoDiscoId
|
|
}).ToList();
|
|
|
|
// 3. AGRUPAR Y CONTAR DISCOS (del cliente y de la BD)
|
|
// Crea un diccionario estilo: {"SSD_256": 2, "HDD_1024": 1}
|
|
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<string, (string anterior, string nuevo)>();
|
|
|
|
// 4. CALCULAR Y EJECUTAR ELIMINACIONES
|
|
var discosAEliminar = new List<int>();
|
|
foreach (var discoDb in discosEnDb)
|
|
{
|
|
var key = $"{discoDb.Mediatype}_{discoDb.Size}";
|
|
if (discosClienteContados.TryGetValue(key, out int count) && count > 0)
|
|
{
|
|
// Este disco todavía existe en el cliente, decrementamos el contador y lo saltamos.
|
|
discosClienteContados[key]--;
|
|
}
|
|
else
|
|
{
|
|
// Este disco ya no está en el cliente, marcamos su asociación para eliminar.
|
|
discosAEliminar.Add(discoDb.EquipoDiscoId);
|
|
|
|
// Registrar para el historial
|
|
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);
|
|
}
|
|
|
|
// 5. CALCULAR Y EJECUTAR INSERCIONES
|
|
foreach (var discoCliente in discosDesdeCliente)
|
|
{
|
|
var key = $"{discoCliente.Mediatype}_{discoCliente.Size}";
|
|
if (discosDbContados.TryGetValue(key, out int count) && count > 0)
|
|
{
|
|
// Este disco ya existía, decrementamos para no volver a añadirlo.
|
|
discosDbContados[key]--;
|
|
}
|
|
else
|
|
{
|
|
// Este es un disco nuevo que hay que asociar.
|
|
var disco = await connection.QuerySingleOrDefaultAsync<Disco>("SELECT * FROM dbo.discos WHERE Mediatype = @Mediatype AND Size = @Size;", discoCliente, transaction);
|
|
if (disco == null) continue; // Si el disco no existe en la tabla maestra, lo ignoramos
|
|
|
|
await connection.ExecuteAsync("INSERT INTO dbo.equipos_discos (equipo_id, disco_id) VALUES (@EquipoId, @DiscoId);", new { EquipoId = equipo.Id, DiscoId = disco.Id }, transaction);
|
|
|
|
// Registrar para el historial
|
|
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());
|
|
}
|
|
}
|
|
|
|
// 6. REGISTRAR CAMBIOS Y CONFIRMAR TRANSACCIÓN
|
|
if (cambios.Count > 0)
|
|
{
|
|
// Formateamos los valores para el historial
|
|
var cambiosFormateados = cambios.ToDictionary(
|
|
kvp => kvp.Key,
|
|
kvp => ($"{kvp.Value.anterior} Instalados", $"{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();
|
|
// Loggear el error en el servidor
|
|
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<IActionResult> AsociarRam(string hostname, [FromBody] List<MemoriaRamDetalle> 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<IActionResult> 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; } }
|
|
} |