ste commit introduce un sistema completo de autenticación basado en JSON Web Tokens (JWT) para proteger los endpoints de la API y gestionar el acceso de los usuarios a la aplicación.
**Cambios en el Backend (ASP.NET Core):**
- Se ha creado un nuevo `AuthController` con un endpoint `POST /api/auth/login` para validar las credenciales del usuario.
- Implementada la generación de tokens JWT con una clave secreta y emisor/audiencia configurables desde `appsettings.json`.
- Se ha añadido una lógica de expiración dinámica para los tokens:
- **6 horas** para sesiones temporales (si el usuario no marca "Mantener sesión").
- **1 año** para sesiones persistentes.
- Se han protegido todos los controladores existentes (`EquiposController`, `SectoresController`, etc.) con el atributo `[Authorize]`, requiriendo un token válido para su acceso.
- Actualizada la configuración de Swagger para incluir un campo de autorización "Bearer Token", facilitando las pruebas de los endpoints protegidos desde la UI.
**Cambios en el Frontend (React):**
- Se ha creado un componente `Login.tsx` que actúa como la puerta de entrada a la aplicación.
- Implementado un `AuthContext` para gestionar el estado global de autenticación (`isAuthenticated`, `token`, `isLoading`).
- Añadida la funcionalidad "Mantener sesión iniciada" a través de un checkbox en el formulario de login.
- Si está marcado, el token se guarda en `localStorage`.
- Si está desmarcado, el token se guarda en `sessionStorage` (la sesión se cierra al cerrar el navegador/pestaña).
- La función `request` en `apiService.ts` ha sido refactorizada para inyectar automáticamente el `Authorization: Bearer <token>` en todas las peticiones a la API.
- Se ha añadido un botón de "Cerrar Sesión" en la barra de navegación que limpia el token y redirige al login.
- Corregido un bug que provocaba un bucle de recarga infinito después de un inicio de sesión exitoso debido a una condición de carrera.
977 lines
42 KiB
C#
977 lines
42 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;
|
|
using Renci.SshNet;
|
|
using System.Text.RegularExpressions;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
|
|
namespace Inventario.API.Controllers
|
|
{
|
|
[Authorize]
|
|
[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<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, 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<int, Equipo>();
|
|
|
|
await connection.QueryAsync<Equipo, Sector, UsuarioEquipoDetalle, DiscoDetalle, MemoriaRamEquipoDetalle, 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.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<IActionResult> 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<int, Equipo>();
|
|
|
|
var equipo = (await connection.QueryAsync<Equipo, Sector, UsuarioEquipoDetalle, 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.");
|
|
|
|
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<DiscoDetalle>(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<MemoriaRamEquipoDetalle>(ramQuery, new { equipo.Id })).ToList();
|
|
|
|
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, 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<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)
|
|
{
|
|
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, 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<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)
|
|
{
|
|
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.");
|
|
}
|
|
|
|
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<DiscoAsociado>(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<string, (string? anterior, string? nuevo)>();
|
|
|
|
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)
|
|
{
|
|
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<Disco>("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<IActionResult> AsociarRam(string hostname, [FromBody] List<MemoriaRamEquipoDetalle> memoriasDesdeCliente)
|
|
{
|
|
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.");
|
|
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<dynamic>(ramActualQuery, new { EquipoId = equipo.Id }, transaction)).ToList();
|
|
|
|
Func<dynamic, string> crearHuella = ram => $"{ram.Slot}_{ram.PartNumber ?? ""}_{ram.Tamano}_{ram.Velocidad ?? 0}";
|
|
var huellasCliente = new HashSet<string>(memoriasDesdeCliente.Select(crearHuella));
|
|
var huellasDb = new HashSet<string>(ramEnDb.Select(crearHuella));
|
|
|
|
var cambios = new Dictionary<string, (string? anterior, string? nuevo)>();
|
|
|
|
Func<dynamic, string> formatRamDetails = ram =>
|
|
{
|
|
var parts = new List<string?> { 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<MemoriaRam>(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<int>(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<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);
|
|
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<string>("SshSettings:Host");
|
|
var sshPort = _configuration.GetValue<int>("SshSettings:Port");
|
|
var sshUser = _configuration.GetValue<string>("SshSettings:User");
|
|
var sshPass = _configuration.GetValue<string>("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<IActionResult> 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<int?>(findQuery, new { equipoDto.Hostname });
|
|
if (existente.HasValue)
|
|
{
|
|
return Conflict($"El hostname '{equipoDto.Hostname}' ya existe.");
|
|
}
|
|
|
|
var nuevoId = await connection.ExecuteScalarAsync<int>(insertQuery, equipoDto);
|
|
|
|
await HistorialHelper.RegistrarCambioUnico(_context, nuevoId, "Equipo", null, "Equipo creado manualmente");
|
|
|
|
var nuevoEquipo = await connection.QuerySingleOrDefaultAsync<Equipo>("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<IActionResult> 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<IActionResult> 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<IActionResult> BorrarAsociacionUsuario(int equipoId, int usuarioId)
|
|
{
|
|
using (var connection = _context.CreateConnection())
|
|
{
|
|
var username = await connection.QuerySingleOrDefaultAsync<string>("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<IActionResult> ActualizarEquipoManual(int id, [FromBody] EditarEquipoManualDto equipoDto)
|
|
{
|
|
using (var connection = _context.CreateConnection())
|
|
{
|
|
var equipoActual = await connection.QuerySingleOrDefaultAsync<Equipo>("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<int?>("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<Sector>("SELECT Id, Nombre FROM dbo.sectores;");
|
|
var sectorMap = allSectores.ToDictionary(s => s.Id, s => s.Nombre);
|
|
|
|
var cambios = new Dictionary<string, (string? anterior, string? nuevo)>();
|
|
|
|
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<Equipo>(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<IActionResult> AsociarDiscoManual(int equipoId, [FromBody] AsociarDiscoManualDto dto)
|
|
{
|
|
using (var connection = _context.CreateConnection())
|
|
{
|
|
var equipo = await connection.QueryFirstOrDefaultAsync<Equipo>("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<Disco>("SELECT * FROM dbo.discos WHERE Mediatype = @Mediatype AND Size = @Size", dto);
|
|
int discoId;
|
|
if (discoMaestro == null)
|
|
{
|
|
discoId = await connection.ExecuteScalarAsync<int>("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<int>(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<IActionResult> AsociarRamManual(int equipoId, [FromBody] AsociarRamManualDto dto)
|
|
{
|
|
using (var connection = _context.CreateConnection())
|
|
{
|
|
var equipo = await connection.QueryFirstOrDefaultAsync<Equipo>("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<MemoriaRam>(
|
|
"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<int>(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<int>(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<IActionResult> AsociarUsuarioManual(int equipoId, [FromBody] AsociarUsuarioManualDto dto)
|
|
{
|
|
using (var connection = _context.CreateConnection())
|
|
{
|
|
var equipo = await connection.QueryFirstOrDefaultAsync<Equipo>("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<Usuario>("SELECT * FROM dbo.usuarios WHERE Username = @Username", dto);
|
|
if (usuario == null)
|
|
{
|
|
usuarioId = await connection.ExecuteScalarAsync<int>("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<IActionResult> GetDistinctFieldValues(string fieldName)
|
|
{
|
|
// 1. Lista blanca de campos permitidos para evitar inyección SQL y exposición de datos.
|
|
var allowedFields = new List<string> { "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<string>(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; }
|
|
}
|
|
}
|
|
} |