Files
Inventario-IT/backend/Controllers/AdminController.cs
dmolinari bc9a9906c3 feat: Sistema de autenticación por JWT
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.
2025-10-13 10:40:20 -03:00

204 lines
6.6 KiB
C#

// backend/Controllers/AdminController.cs
using Dapper;
using Inventario.API.Data;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
namespace Inventario.API.Controllers
{
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class AdminController : ControllerBase
{
private readonly DapperContext _context;
public AdminController(DapperContext context)
{
_context = context;
}
// --- DTOs para los componentes ---
public class ComponenteValorDto
{
public string Valor { get; set; } = "";
public int Conteo { get; set; }
}
public class UnificarComponenteDto
{
public required string ValorNuevo { get; set; }
public required string ValorAntiguo { get; set; }
}
public class RamAgrupadaDto
{
public string? Fabricante { get; set; }
public int Tamano { get; set; }
public int? Velocidad { get; set; }
public int Conteo { get; set; }
}
public class BorrarRamAgrupadaDto
{
public string? Fabricante { get; set; }
public int Tamano { get; set; }
public int? Velocidad { get; set; }
}
[HttpGet("componentes/{tipo}")]
public async Task<IActionResult> GetComponenteValores(string tipo)
{
var allowedTypes = new Dictionary<string, string>
{
{ "os", "Os" },
{ "cpu", "Cpu" },
{ "motherboard", "Motherboard" },
{ "architecture", "Architecture" }
};
if (!allowedTypes.TryGetValue(tipo.ToLower(), out var columnName))
{
return BadRequest("Tipo de componente no válido.");
}
var query = $@"
SELECT {columnName} AS Valor, COUNT(*) AS Conteo
FROM dbo.equipos
WHERE {columnName} IS NOT NULL AND {columnName} != ''
GROUP BY {columnName}
ORDER BY Conteo DESC, Valor ASC;";
using (var connection = _context.CreateConnection())
{
var valores = await connection.QueryAsync<ComponenteValorDto>(query);
return Ok(valores);
}
}
[HttpPut("componentes/{tipo}/unificar")]
public async Task<IActionResult> UnificarComponenteValores(string tipo, [FromBody] UnificarComponenteDto dto)
{
var allowedTypes = new Dictionary<string, string>
{
{ "os", "Os" },
{ "cpu", "Cpu" },
{ "motherboard", "Motherboard" },
{ "architecture", "Architecture" }
};
if (!allowedTypes.TryGetValue(tipo.ToLower(), out var columnName))
{
return BadRequest("Tipo de componente no válido.");
}
if (dto.ValorAntiguo == dto.ValorNuevo)
{
return BadRequest("El valor antiguo y el nuevo no pueden ser iguales.");
}
var query = $@"
UPDATE dbo.equipos
SET {columnName} = @ValorNuevo
WHERE {columnName} = @ValorAntiguo;";
using (var connection = _context.CreateConnection())
{
var filasAfectadas = await connection.ExecuteAsync(query, new { dto.ValorNuevo, dto.ValorAntiguo });
return Ok(new { message = $"Se unificaron {filasAfectadas} registros.", filasAfectadas });
}
}
// --- Devuelve la RAM agrupada ---
[HttpGet("componentes/ram")]
public async Task<IActionResult> GetComponentesRam()
{
var query = @"
SELECT
mr.Fabricante,
mr.Tamano,
mr.Velocidad,
COUNT(emr.memoria_ram_id) as Conteo
FROM
dbo.memorias_ram mr
LEFT JOIN
dbo.equipos_memorias_ram emr ON mr.id = emr.memoria_ram_id
GROUP BY
mr.Fabricante,
mr.Tamano,
mr.Velocidad
ORDER BY
Conteo DESC, mr.Fabricante, mr.Tamano;";
using (var connection = _context.CreateConnection())
{
var valores = await connection.QueryAsync<RamAgrupadaDto>(query);
return Ok(valores);
}
}
// --- Elimina un grupo completo ---
[HttpDelete("componentes/ram")]
public async Task<IActionResult> BorrarComponenteRam([FromBody] BorrarRamAgrupadaDto dto)
{
using (var connection = _context.CreateConnection())
{
// Verificación de seguridad: Asegurarse de que el grupo no esté en uso.
var usageQuery = @"
SELECT COUNT(emr.id)
FROM dbo.memorias_ram mr
LEFT JOIN dbo.equipos_memorias_ram emr ON mr.id = emr.memoria_ram_id
WHERE (mr.Fabricante = @Fabricante OR (mr.Fabricante IS NULL AND @Fabricante IS NULL))
AND mr.Tamano = @Tamano
AND (mr.Velocidad = @Velocidad OR (mr.Velocidad IS NULL AND @Velocidad IS NULL));";
var usageCount = await connection.ExecuteScalarAsync<int>(usageQuery, dto);
if (usageCount > 0)
{
return Conflict(new { message = $"Este grupo de RAM está en uso por {usageCount} equipo(s) y no puede ser eliminado." });
}
// Si no está en uso, proceder con la eliminación de todos los registros maestros que coincidan.
var deleteQuery = @"
DELETE 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));";
await connection.ExecuteAsync(deleteQuery, dto);
return NoContent();
}
}
[HttpDelete("componentes/{tipo}/{valor}")]
public async Task<IActionResult> BorrarComponenteTexto(string tipo, string valor)
{
var allowedTypes = new Dictionary<string, string>
{
{ "os", "Os" },
{ "cpu", "Cpu" },
{ "motherboard", "Motherboard" },
{ "architecture", "Architecture" }
};
if (!allowedTypes.TryGetValue(tipo.ToLower(), out var columnName))
{
return BadRequest("Tipo de componente no válido.");
}
using (var connection = _context.CreateConnection())
{
var usageQuery = $"SELECT COUNT(*) FROM dbo.equipos WHERE {columnName} = @Valor;";
var usageCount = await connection.ExecuteScalarAsync<int>(usageQuery, new { Valor = valor });
if (usageCount > 0)
{
return Conflict(new { message = $"Este valor está en uso por {usageCount} equipo(s) y no puede ser eliminado. Intente unificarlo en su lugar." });
}
return NoContent();
}
}
}
}