From bc9a9906c340b64a7f24447bca4eb12f45689879 Mon Sep 17 00:00:00 2001 From: dmolinari Date: Mon, 13 Oct 2025 10:40:20 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20Sistema=20de=20autenticaci=C3=B3n=20por?= =?UTF-8?q?=20JWT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 ` 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. --- backend/Controllers/AdminController.cs | 2 + backend/Controllers/AuthController.cs | 70 +++++++ backend/Controllers/DashboardController.cs | 2 + backend/Controllers/DiscosController.cs | 2 + backend/Controllers/EquiposController.cs | 2 + backend/Controllers/MemoriasRamController.cs | 2 + backend/Controllers/SectoresController.cs | 2 + backend/Controllers/UsuariosController.cs | 2 + backend/Inventario.API.csproj | 1 + backend/Program.cs | 63 +++++- backend/appsettings.Development.json | 9 + backend/appsettings.json | 9 + .../net9.0/Inventario.API.AssemblyInfo.cs | 2 +- .../Inventario.API.csproj.nuget.dgspec.json | 4 + backend/obj/project.assets.json | 182 +++++++++++------- frontend/src/App.tsx | 12 ++ frontend/src/components/Login.tsx | 75 ++++++++ frontend/src/components/Navbar.tsx | 19 +- frontend/src/context/AuthContext.tsx | 64 ++++++ frontend/src/main.tsx | 39 ++-- frontend/src/services/apiService.ts | 40 +++- 21 files changed, 504 insertions(+), 99 deletions(-) create mode 100644 backend/Controllers/AuthController.cs create mode 100644 frontend/src/components/Login.tsx create mode 100644 frontend/src/context/AuthContext.tsx diff --git a/backend/Controllers/AdminController.cs b/backend/Controllers/AdminController.cs index cd26301..9c4fc66 100644 --- a/backend/Controllers/AdminController.cs +++ b/backend/Controllers/AdminController.cs @@ -2,9 +2,11 @@ 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 diff --git a/backend/Controllers/AuthController.cs b/backend/Controllers/AuthController.cs new file mode 100644 index 0000000..7ee5127 --- /dev/null +++ b/backend/Controllers/AuthController.cs @@ -0,0 +1,70 @@ +// backend/Controllers/AuthController.cs +using Microsoft.AspNetCore.Mvc; +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; + +namespace Inventario.API.Controllers +{ + // DTO para recibir las credenciales del usuario + public class LoginDto + { + public required string Username { get; set; } + public required string Password { get; set; } + public bool RememberMe { get; set; } + } + + [ApiController] + [Route("api/[controller]")] + public class AuthController : ControllerBase + { + private readonly IConfiguration _config; + + public AuthController(IConfiguration config) + { + _config = config; + } + + [HttpPost("login")] + public IActionResult Login([FromBody] LoginDto login) + { + if (login.Username == _config["AuthSettings:Username"] && login.Password == _config["AuthSettings:Password"]) + { + // Pasamos el valor de RememberMe a la función de generación + var token = GenerateJwtToken(login.Username, login.RememberMe); + return Ok(new { token }); + } + return Unauthorized(new { message = "Credenciales inválidas." }); + } + + private string GenerateJwtToken(string username, bool rememberMe) + { + var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]!)); + var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256); + + var claims = new[] + { + new Claim(JwtRegisteredClaimNames.Sub, username), + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) + }; + + // --- LÓGICA DE EXPIRACIÓN DINÁMICA --- + // Si "rememberMe" es true, expira en 1 año. + // Si es false, expira en 6 horas. + var expirationTime = rememberMe + ? DateTime.Now.AddYears(1) + : DateTime.Now.AddHours(6); + // ------------------------------------ + + var token = new JwtSecurityToken( + issuer: _config["Jwt:Issuer"], + audience: _config["Jwt:Audience"], + claims: claims, + expires: expirationTime, + signingCredentials: credentials); + + return new JwtSecurityTokenHandler().WriteToken(token); + } + } +} \ No newline at end of file diff --git a/backend/Controllers/DashboardController.cs b/backend/Controllers/DashboardController.cs index 6125f3a..7ad0ab7 100644 --- a/backend/Controllers/DashboardController.cs +++ b/backend/Controllers/DashboardController.cs @@ -1,9 +1,11 @@ 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 DashboardController : ControllerBase diff --git a/backend/Controllers/DiscosController.cs b/backend/Controllers/DiscosController.cs index 6348b40..69222b8 100644 --- a/backend/Controllers/DiscosController.cs +++ b/backend/Controllers/DiscosController.cs @@ -2,9 +2,11 @@ using Dapper; using Inventario.API.Data; using Inventario.API.Models; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; namespace Inventario.API.Controllers { + [Authorize] [ApiController] [Route("api/[controller]")] public class DiscosController : ControllerBase diff --git a/backend/Controllers/EquiposController.cs b/backend/Controllers/EquiposController.cs index 1137b94..90a2112 100644 --- a/backend/Controllers/EquiposController.cs +++ b/backend/Controllers/EquiposController.cs @@ -9,9 +9,11 @@ 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 diff --git a/backend/Controllers/MemoriasRamController.cs b/backend/Controllers/MemoriasRamController.cs index a18bf20..6b43827 100644 --- a/backend/Controllers/MemoriasRamController.cs +++ b/backend/Controllers/MemoriasRamController.cs @@ -4,9 +4,11 @@ using Dapper; using Inventario.API.Data; using Inventario.API.Models; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; namespace Inventario.API.Controllers { + [Authorize] [ApiController] [Route("api/[controller]")] public class MemoriasRamController : ControllerBase diff --git a/backend/Controllers/SectoresController.cs b/backend/Controllers/SectoresController.cs index c7a0c60..66031b7 100644 --- a/backend/Controllers/SectoresController.cs +++ b/backend/Controllers/SectoresController.cs @@ -4,9 +4,11 @@ using Dapper; using Inventario.API.Data; using Inventario.API.Models; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; namespace Inventario.API.Controllers { + [Authorize] [ApiController] [Route("api/[controller]")] public class SectoresController : ControllerBase diff --git a/backend/Controllers/UsuariosController.cs b/backend/Controllers/UsuariosController.cs index a62fabe..a9abdc4 100644 --- a/backend/Controllers/UsuariosController.cs +++ b/backend/Controllers/UsuariosController.cs @@ -2,9 +2,11 @@ using Dapper; using Inventario.API.Data; using Inventario.API.Models; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; namespace Inventario.API.Controllers { + [Authorize] [ApiController] [Route("api/[controller]")] public class UsuariosController : ControllerBase diff --git a/backend/Inventario.API.csproj b/backend/Inventario.API.csproj index 9b01086..54b62e6 100644 --- a/backend/Inventario.API.csproj +++ b/backend/Inventario.API.csproj @@ -8,6 +8,7 @@ + diff --git a/backend/Program.cs b/backend/Program.cs index ce83e69..11454dd 100644 --- a/backend/Program.cs +++ b/backend/Program.cs @@ -1,13 +1,67 @@ +// backend/Program.cs using Inventario.API.Data; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; +using System.Text; +using Microsoft.OpenApi.Models; var builder = WebApplication.CreateBuilder(args); +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = builder.Configuration["Jwt:Issuer"], + ValidAudience = builder.Configuration["Jwt:Audience"], + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!)) + }; + }); + // Add services to the container. builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); -// --- 1. DEFINIR LA POLÍTICA CORS --- +// CONFIGURACIÓN DE SWAGGER +builder.Services.AddSwaggerGen(options => +{ + // 1. Definir el esquema de seguridad (JWT Bearer) + options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + Description = "Autenticación JWT usando el esquema Bearer. " + + "Introduce 'Bearer' [espacio] y luego tu token en el campo de abajo. " + + "Ejemplo: 'Bearer 12345abcdef'", + Name = "Authorization", // El nombre del header + In = ParameterLocation.Header, // Dónde se envía (en la cabecera) + Type = SecuritySchemeType.ApiKey, // Tipo de esquema + Scheme = "Bearer" + }); + + // 2. Aplicar el requisito de seguridad globalmente a todos los endpoints + options.AddSecurityRequirement(new OpenApiSecurityRequirement() + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" // Debe coincidir con el Id de AddSecurityDefinition + }, + Scheme = "oauth2", + Name = "Bearer", + In = ParameterLocation.Header, + }, + new List() + } + }); +}); + +// --- DEFINIR LA POLÍTICA CORS --- // Definimos un nombre para nuestra política var MyAllowSpecificOrigins = "_myAllowSpecificOrigins"; @@ -42,11 +96,14 @@ if (app.Environment.IsDevelopment()) app.UseHttpsRedirection(); -// --- 2. ACTIVAR EL MIDDLEWARE DE CORS --- +// --- ACTIVAR EL MIDDLEWARE DE CORS --- // ¡IMPORTANTE! Debe ir ANTES de MapControllers y DESPUÉS de UseHttpsRedirection (si se usa) app.UseCors(MyAllowSpecificOrigins); // ---------------------------------------- +app.UseAuthentication(); +app.UseAuthorization(); + app.MapControllers(); app.Run(); \ No newline at end of file diff --git a/backend/appsettings.Development.json b/backend/appsettings.Development.json index f171ea0..c1c9087 100644 --- a/backend/appsettings.Development.json +++ b/backend/appsettings.Development.json @@ -5,6 +5,15 @@ "Microsoft.AspNetCore": "Warning" } }, + "AuthSettings": { + "Username": "admin", + "Password": "PTP847Equipos" + }, + "Jwt": { + "Key": "badb1a38d221c9e23bcf70958840ca7f5a5dc54f2047dadf7ce45b578b5bc3e2", + "Issuer": "InventarioAPI", + "Audience": "InventarioClient" + }, "ConnectionStrings": { "DefaultConnection": "Server=TECNICA3;Database=InventarioDB;User Id=apiequipos;Password=@Apiequipos513@;TrustServerCertificate=True" }, diff --git a/backend/appsettings.json b/backend/appsettings.json index 154b777..8e83158 100644 --- a/backend/appsettings.json +++ b/backend/appsettings.json @@ -6,6 +6,15 @@ } }, "AllowedHosts": "*", + "AuthSettings": { + "Username": "admin", + "Password": "PTP847Equipos" + }, + "Jwt": { + "Key": "badb1a38d221c9e23bcf70958840ca7f5a5dc54f2047dadf7ce45b578b5bc3e2", + "Issuer": "InventarioAPI", + "Audience": "InventarioClient" + }, "ConnectionStrings": { "DefaultConnection": "Server=db-sqlserver;Database=InventarioDB;User Id=apiequipos;Password=@Apiequipos513@;TrustServerCertificate=True" }, diff --git a/backend/obj/Debug/net9.0/Inventario.API.AssemblyInfo.cs b/backend/obj/Debug/net9.0/Inventario.API.AssemblyInfo.cs index 6ce36c7..f9b5276 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+8162d59331f63963077dd822669378174380b386")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+acf2f9a35c8a559db55e21ce6dd2066c30a01669")] [assembly: System.Reflection.AssemblyProductAttribute("Inventario.API")] [assembly: System.Reflection.AssemblyTitleAttribute("Inventario.API")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] diff --git a/backend/obj/Inventario.API.csproj.nuget.dgspec.json b/backend/obj/Inventario.API.csproj.nuget.dgspec.json index 23572bd..15cc9a0 100644 --- a/backend/obj/Inventario.API.csproj.nuget.dgspec.json +++ b/backend/obj/Inventario.API.csproj.nuget.dgspec.json @@ -54,6 +54,10 @@ "target": "Package", "version": "[2.1.66, )" }, + "Microsoft.AspNetCore.Authentication.JwtBearer": { + "target": "Package", + "version": "[9.0.9, )" + }, "Microsoft.AspNetCore.OpenApi": { "target": "Package", "version": "[9.0.5, )" diff --git a/backend/obj/project.assets.json b/backend/obj/project.assets.json index 4b6d23a..c977358 100644 --- a/backend/obj/project.assets.json +++ b/backend/obj/project.assets.json @@ -78,6 +78,25 @@ } } }, + "Microsoft.AspNetCore.Authentication.JwtBearer/9.0.9": { + "type": "package", + "dependencies": { + "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.0.1" + }, + "compile": { + "lib/net9.0/Microsoft.AspNetCore.Authentication.JwtBearer.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net9.0/Microsoft.AspNetCore.Authentication.JwtBearer.dll": { + "related": ".xml" + } + }, + "frameworkReferences": [ + "Microsoft.AspNetCore.App" + ] + }, "Microsoft.AspNetCore.OpenApi/9.0.5": { "type": "package", "dependencies": { @@ -897,96 +916,96 @@ } } }, - "Microsoft.IdentityModel.Abstractions/7.7.1": { + "Microsoft.IdentityModel.Abstractions/8.0.1": { "type": "package", "compile": { - "lib/net8.0/Microsoft.IdentityModel.Abstractions.dll": { + "lib/net9.0/Microsoft.IdentityModel.Abstractions.dll": { "related": ".xml" } }, "runtime": { - "lib/net8.0/Microsoft.IdentityModel.Abstractions.dll": { + "lib/net9.0/Microsoft.IdentityModel.Abstractions.dll": { "related": ".xml" } } }, - "Microsoft.IdentityModel.JsonWebTokens/7.7.1": { + "Microsoft.IdentityModel.JsonWebTokens/8.0.1": { "type": "package", "dependencies": { - "Microsoft.IdentityModel.Tokens": "7.7.1" + "Microsoft.IdentityModel.Tokens": "8.0.1" }, "compile": { - "lib/net8.0/Microsoft.IdentityModel.JsonWebTokens.dll": { + "lib/net9.0/Microsoft.IdentityModel.JsonWebTokens.dll": { "related": ".xml" } }, "runtime": { - "lib/net8.0/Microsoft.IdentityModel.JsonWebTokens.dll": { + "lib/net9.0/Microsoft.IdentityModel.JsonWebTokens.dll": { "related": ".xml" } } }, - "Microsoft.IdentityModel.Logging/7.7.1": { + "Microsoft.IdentityModel.Logging/8.0.1": { "type": "package", "dependencies": { - "Microsoft.IdentityModel.Abstractions": "7.7.1" + "Microsoft.IdentityModel.Abstractions": "8.0.1" }, "compile": { - "lib/net8.0/Microsoft.IdentityModel.Logging.dll": { + "lib/net9.0/Microsoft.IdentityModel.Logging.dll": { "related": ".xml" } }, "runtime": { - "lib/net8.0/Microsoft.IdentityModel.Logging.dll": { + "lib/net9.0/Microsoft.IdentityModel.Logging.dll": { "related": ".xml" } } }, - "Microsoft.IdentityModel.Protocols/7.7.1": { + "Microsoft.IdentityModel.Protocols/8.0.1": { "type": "package", "dependencies": { - "Microsoft.IdentityModel.Tokens": "7.7.1" + "Microsoft.IdentityModel.Tokens": "8.0.1" }, "compile": { - "lib/net8.0/Microsoft.IdentityModel.Protocols.dll": { + "lib/net9.0/Microsoft.IdentityModel.Protocols.dll": { "related": ".xml" } }, "runtime": { - "lib/net8.0/Microsoft.IdentityModel.Protocols.dll": { + "lib/net9.0/Microsoft.IdentityModel.Protocols.dll": { "related": ".xml" } } }, - "Microsoft.IdentityModel.Protocols.OpenIdConnect/7.7.1": { + "Microsoft.IdentityModel.Protocols.OpenIdConnect/8.0.1": { "type": "package", "dependencies": { - "Microsoft.IdentityModel.Protocols": "7.7.1", - "System.IdentityModel.Tokens.Jwt": "7.7.1" + "Microsoft.IdentityModel.Protocols": "8.0.1", + "System.IdentityModel.Tokens.Jwt": "8.0.1" }, "compile": { - "lib/net8.0/Microsoft.IdentityModel.Protocols.OpenIdConnect.dll": { + "lib/net9.0/Microsoft.IdentityModel.Protocols.OpenIdConnect.dll": { "related": ".xml" } }, "runtime": { - "lib/net8.0/Microsoft.IdentityModel.Protocols.OpenIdConnect.dll": { + "lib/net9.0/Microsoft.IdentityModel.Protocols.OpenIdConnect.dll": { "related": ".xml" } } }, - "Microsoft.IdentityModel.Tokens/7.7.1": { + "Microsoft.IdentityModel.Tokens/8.0.1": { "type": "package", "dependencies": { - "Microsoft.IdentityModel.Logging": "7.7.1" + "Microsoft.IdentityModel.Logging": "8.0.1" }, "compile": { - "lib/net8.0/Microsoft.IdentityModel.Tokens.dll": { + "lib/net9.0/Microsoft.IdentityModel.Tokens.dll": { "related": ".xml" } }, "runtime": { - "lib/net8.0/Microsoft.IdentityModel.Tokens.dll": { + "lib/net9.0/Microsoft.IdentityModel.Tokens.dll": { "related": ".xml" } } @@ -1355,19 +1374,19 @@ "buildTransitive/net8.0/_._": {} } }, - "System.IdentityModel.Tokens.Jwt/7.7.1": { + "System.IdentityModel.Tokens.Jwt/8.0.1": { "type": "package", "dependencies": { - "Microsoft.IdentityModel.JsonWebTokens": "7.7.1", - "Microsoft.IdentityModel.Tokens": "7.7.1" + "Microsoft.IdentityModel.JsonWebTokens": "8.0.1", + "Microsoft.IdentityModel.Tokens": "8.0.1" }, "compile": { - "lib/net8.0/System.IdentityModel.Tokens.Jwt.dll": { + "lib/net9.0/System.IdentityModel.Tokens.Jwt.dll": { "related": ".xml" } }, "runtime": { - "lib/net8.0/System.IdentityModel.Tokens.Jwt.dll": { + "lib/net9.0/System.IdentityModel.Tokens.Jwt.dll": { "related": ".xml" } } @@ -1618,6 +1637,22 @@ "logo.png" ] }, + "Microsoft.AspNetCore.Authentication.JwtBearer/9.0.9": { + "sha512": "U5gW2DS/yAE9X0Ko63/O2lNApAzI/jhx4IT1Th6W0RShKv6XAVVgLGN3zqnmcd6DtAnp5FYs+4HZrxsTl0anLA==", + "type": "package", + "path": "microsoft.aspnetcore.authentication.jwtbearer/9.0.9", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "Icon.png", + "PACKAGE.md", + "THIRD-PARTY-NOTICES.TXT", + "lib/net9.0/Microsoft.AspNetCore.Authentication.JwtBearer.dll", + "lib/net9.0/Microsoft.AspNetCore.Authentication.JwtBearer.xml", + "microsoft.aspnetcore.authentication.jwtbearer.9.0.9.nupkg.sha512", + "microsoft.aspnetcore.authentication.jwtbearer.nuspec" + ] + }, "Microsoft.AspNetCore.OpenApi/9.0.5": { "sha512": "yZLOciYlpaOO/mHPOpgeSZTv8Lc7fOOVX40eWJJoGs/S9Ny9CymDuKKQofGE9stXGGM9EEnnuPeq0fhR8kdFfg==", "type": "package", @@ -3469,15 +3504,13 @@ "microsoft.identity.client.extensions.msal.nuspec" ] }, - "Microsoft.IdentityModel.Abstractions/7.7.1": { - "sha512": "S7sHg6gLg7oFqNGLwN1qSbJDI+QcRRj8SuJ1jHyCmKSipnF6ZQL+tFV2NzVfGj/xmGT9TykQdQiBN+p5Idl4TA==", + "Microsoft.IdentityModel.Abstractions/8.0.1": { + "sha512": "OtlIWcyX01olfdevPKZdIPfBEvbcioDyBiE/Z2lHsopsMD7twcKtlN9kMevHmI5IIPhFpfwCIiR6qHQz1WHUIw==", "type": "package", - "path": "microsoft.identitymodel.abstractions/7.7.1", + "path": "microsoft.identitymodel.abstractions/8.0.1", "files": [ ".nupkg.metadata", ".signature.p7s", - "lib/net461/Microsoft.IdentityModel.Abstractions.dll", - "lib/net461/Microsoft.IdentityModel.Abstractions.xml", "lib/net462/Microsoft.IdentityModel.Abstractions.dll", "lib/net462/Microsoft.IdentityModel.Abstractions.xml", "lib/net472/Microsoft.IdentityModel.Abstractions.dll", @@ -3486,21 +3519,21 @@ "lib/net6.0/Microsoft.IdentityModel.Abstractions.xml", "lib/net8.0/Microsoft.IdentityModel.Abstractions.dll", "lib/net8.0/Microsoft.IdentityModel.Abstractions.xml", + "lib/net9.0/Microsoft.IdentityModel.Abstractions.dll", + "lib/net9.0/Microsoft.IdentityModel.Abstractions.xml", "lib/netstandard2.0/Microsoft.IdentityModel.Abstractions.dll", "lib/netstandard2.0/Microsoft.IdentityModel.Abstractions.xml", - "microsoft.identitymodel.abstractions.7.7.1.nupkg.sha512", + "microsoft.identitymodel.abstractions.8.0.1.nupkg.sha512", "microsoft.identitymodel.abstractions.nuspec" ] }, - "Microsoft.IdentityModel.JsonWebTokens/7.7.1": { - "sha512": "3Izi75UCUssvo8LPx3OVnEeZay58qaFicrtSnbtUt7q8qQi0gy46gh4V8VUTkMVMKXV6VMyjBVmeNNgeCUJuIw==", + "Microsoft.IdentityModel.JsonWebTokens/8.0.1": { + "sha512": "s6++gF9x0rQApQzOBbSyp4jUaAlwm+DroKfL8gdOHxs83k8SJfUXhuc46rDB3rNXBQ1MVRxqKUrqFhO/M0E97g==", "type": "package", - "path": "microsoft.identitymodel.jsonwebtokens/7.7.1", + "path": "microsoft.identitymodel.jsonwebtokens/8.0.1", "files": [ ".nupkg.metadata", ".signature.p7s", - "lib/net461/Microsoft.IdentityModel.JsonWebTokens.dll", - "lib/net461/Microsoft.IdentityModel.JsonWebTokens.xml", "lib/net462/Microsoft.IdentityModel.JsonWebTokens.dll", "lib/net462/Microsoft.IdentityModel.JsonWebTokens.xml", "lib/net472/Microsoft.IdentityModel.JsonWebTokens.dll", @@ -3509,21 +3542,21 @@ "lib/net6.0/Microsoft.IdentityModel.JsonWebTokens.xml", "lib/net8.0/Microsoft.IdentityModel.JsonWebTokens.dll", "lib/net8.0/Microsoft.IdentityModel.JsonWebTokens.xml", + "lib/net9.0/Microsoft.IdentityModel.JsonWebTokens.dll", + "lib/net9.0/Microsoft.IdentityModel.JsonWebTokens.xml", "lib/netstandard2.0/Microsoft.IdentityModel.JsonWebTokens.dll", "lib/netstandard2.0/Microsoft.IdentityModel.JsonWebTokens.xml", - "microsoft.identitymodel.jsonwebtokens.7.7.1.nupkg.sha512", + "microsoft.identitymodel.jsonwebtokens.8.0.1.nupkg.sha512", "microsoft.identitymodel.jsonwebtokens.nuspec" ] }, - "Microsoft.IdentityModel.Logging/7.7.1": { - "sha512": "BZNgSq/o8gsKExdYoBKPR65fdsxW0cTF8PsdqB8y011AGUJJW300S/ZIsEUD0+sOmGc003Gwv3FYbjrVjvsLNQ==", + "Microsoft.IdentityModel.Logging/8.0.1": { + "sha512": "UCPF2exZqBXe7v/6sGNiM6zCQOUXXQ9+v5VTb9gPB8ZSUPnX53BxlN78v2jsbIvK9Dq4GovQxo23x8JgWvm/Qg==", "type": "package", - "path": "microsoft.identitymodel.logging/7.7.1", + "path": "microsoft.identitymodel.logging/8.0.1", "files": [ ".nupkg.metadata", ".signature.p7s", - "lib/net461/Microsoft.IdentityModel.Logging.dll", - "lib/net461/Microsoft.IdentityModel.Logging.xml", "lib/net462/Microsoft.IdentityModel.Logging.dll", "lib/net462/Microsoft.IdentityModel.Logging.xml", "lib/net472/Microsoft.IdentityModel.Logging.dll", @@ -3532,21 +3565,21 @@ "lib/net6.0/Microsoft.IdentityModel.Logging.xml", "lib/net8.0/Microsoft.IdentityModel.Logging.dll", "lib/net8.0/Microsoft.IdentityModel.Logging.xml", + "lib/net9.0/Microsoft.IdentityModel.Logging.dll", + "lib/net9.0/Microsoft.IdentityModel.Logging.xml", "lib/netstandard2.0/Microsoft.IdentityModel.Logging.dll", "lib/netstandard2.0/Microsoft.IdentityModel.Logging.xml", - "microsoft.identitymodel.logging.7.7.1.nupkg.sha512", + "microsoft.identitymodel.logging.8.0.1.nupkg.sha512", "microsoft.identitymodel.logging.nuspec" ] }, - "Microsoft.IdentityModel.Protocols/7.7.1": { - "sha512": "h+fHHBGokepmCX+QZXJk4Ij8OApCb2n2ktoDkNX5CXteXsOxTHMNgjPGpAwdJMFvAL7TtGarUnk3o97NmBq2QQ==", + "Microsoft.IdentityModel.Protocols/8.0.1": { + "sha512": "uA2vpKqU3I2mBBEaeJAWPTjT9v1TZrGWKdgK6G5qJd03CLx83kdiqO9cmiK8/n1erkHzFBwU/RphP83aAe3i3g==", "type": "package", - "path": "microsoft.identitymodel.protocols/7.7.1", + "path": "microsoft.identitymodel.protocols/8.0.1", "files": [ ".nupkg.metadata", ".signature.p7s", - "lib/net461/Microsoft.IdentityModel.Protocols.dll", - "lib/net461/Microsoft.IdentityModel.Protocols.xml", "lib/net462/Microsoft.IdentityModel.Protocols.dll", "lib/net462/Microsoft.IdentityModel.Protocols.xml", "lib/net472/Microsoft.IdentityModel.Protocols.dll", @@ -3555,21 +3588,21 @@ "lib/net6.0/Microsoft.IdentityModel.Protocols.xml", "lib/net8.0/Microsoft.IdentityModel.Protocols.dll", "lib/net8.0/Microsoft.IdentityModel.Protocols.xml", + "lib/net9.0/Microsoft.IdentityModel.Protocols.dll", + "lib/net9.0/Microsoft.IdentityModel.Protocols.xml", "lib/netstandard2.0/Microsoft.IdentityModel.Protocols.dll", "lib/netstandard2.0/Microsoft.IdentityModel.Protocols.xml", - "microsoft.identitymodel.protocols.7.7.1.nupkg.sha512", + "microsoft.identitymodel.protocols.8.0.1.nupkg.sha512", "microsoft.identitymodel.protocols.nuspec" ] }, - "Microsoft.IdentityModel.Protocols.OpenIdConnect/7.7.1": { - "sha512": "yT2Hdj8LpPbcT9C9KlLVxXl09C8zjFaVSaApdOwuecMuoV4s6Sof/mnTDz/+F/lILPIBvrWugR9CC7iRVZgbfQ==", + "Microsoft.IdentityModel.Protocols.OpenIdConnect/8.0.1": { + "sha512": "AQDbfpL+yzuuGhO/mQhKNsp44pm5Jv8/BI4KiFXR7beVGZoSH35zMV3PrmcfvSTsyI6qrcR898NzUauD6SRigg==", "type": "package", - "path": "microsoft.identitymodel.protocols.openidconnect/7.7.1", + "path": "microsoft.identitymodel.protocols.openidconnect/8.0.1", "files": [ ".nupkg.metadata", ".signature.p7s", - "lib/net461/Microsoft.IdentityModel.Protocols.OpenIdConnect.dll", - "lib/net461/Microsoft.IdentityModel.Protocols.OpenIdConnect.xml", "lib/net462/Microsoft.IdentityModel.Protocols.OpenIdConnect.dll", "lib/net462/Microsoft.IdentityModel.Protocols.OpenIdConnect.xml", "lib/net472/Microsoft.IdentityModel.Protocols.OpenIdConnect.dll", @@ -3578,21 +3611,21 @@ "lib/net6.0/Microsoft.IdentityModel.Protocols.OpenIdConnect.xml", "lib/net8.0/Microsoft.IdentityModel.Protocols.OpenIdConnect.dll", "lib/net8.0/Microsoft.IdentityModel.Protocols.OpenIdConnect.xml", + "lib/net9.0/Microsoft.IdentityModel.Protocols.OpenIdConnect.dll", + "lib/net9.0/Microsoft.IdentityModel.Protocols.OpenIdConnect.xml", "lib/netstandard2.0/Microsoft.IdentityModel.Protocols.OpenIdConnect.dll", "lib/netstandard2.0/Microsoft.IdentityModel.Protocols.OpenIdConnect.xml", - "microsoft.identitymodel.protocols.openidconnect.7.7.1.nupkg.sha512", + "microsoft.identitymodel.protocols.openidconnect.8.0.1.nupkg.sha512", "microsoft.identitymodel.protocols.openidconnect.nuspec" ] }, - "Microsoft.IdentityModel.Tokens/7.7.1": { - "sha512": "fQ0VVCba75lknUHGldi3iTKAYUQqbzp1Un8+d9cm9nON0Gs8NAkXddNg8iaUB0qi/ybtAmNWizTR4avdkCJ9pQ==", + "Microsoft.IdentityModel.Tokens/8.0.1": { + "sha512": "kDimB6Dkd3nkW2oZPDkMkVHfQt3IDqO5gL0oa8WVy3OP4uE8Ij+8TXnqg9TOd9ufjsY3IDiGz7pCUbnfL18tjg==", "type": "package", - "path": "microsoft.identitymodel.tokens/7.7.1", + "path": "microsoft.identitymodel.tokens/8.0.1", "files": [ ".nupkg.metadata", ".signature.p7s", - "lib/net461/Microsoft.IdentityModel.Tokens.dll", - "lib/net461/Microsoft.IdentityModel.Tokens.xml", "lib/net462/Microsoft.IdentityModel.Tokens.dll", "lib/net462/Microsoft.IdentityModel.Tokens.xml", "lib/net472/Microsoft.IdentityModel.Tokens.dll", @@ -3601,9 +3634,11 @@ "lib/net6.0/Microsoft.IdentityModel.Tokens.xml", "lib/net8.0/Microsoft.IdentityModel.Tokens.dll", "lib/net8.0/Microsoft.IdentityModel.Tokens.xml", + "lib/net9.0/Microsoft.IdentityModel.Tokens.dll", + "lib/net9.0/Microsoft.IdentityModel.Tokens.xml", "lib/netstandard2.0/Microsoft.IdentityModel.Tokens.dll", "lib/netstandard2.0/Microsoft.IdentityModel.Tokens.xml", - "microsoft.identitymodel.tokens.7.7.1.nupkg.sha512", + "microsoft.identitymodel.tokens.8.0.1.nupkg.sha512", "microsoft.identitymodel.tokens.nuspec" ] }, @@ -4090,15 +4125,13 @@ "useSharedDesignerContext.txt" ] }, - "System.IdentityModel.Tokens.Jwt/7.7.1": { - "sha512": "rQkO1YbAjLwnDJSMpRhRtrc6XwIcEOcUvoEcge+evurpzSZM3UNK+MZfD3sKyTlYsvknZ6eJjSBfnmXqwOsT9Q==", + "System.IdentityModel.Tokens.Jwt/8.0.1": { + "sha512": "GJw3bYkWpOgvN3tJo5X4lYUeIFA2HD293FPUhKmp7qxS+g5ywAb34Dnd3cDAFLkcMohy5XTpoaZ4uAHuw0uSPQ==", "type": "package", - "path": "system.identitymodel.tokens.jwt/7.7.1", + "path": "system.identitymodel.tokens.jwt/8.0.1", "files": [ ".nupkg.metadata", ".signature.p7s", - "lib/net461/System.IdentityModel.Tokens.Jwt.dll", - "lib/net461/System.IdentityModel.Tokens.Jwt.xml", "lib/net462/System.IdentityModel.Tokens.Jwt.dll", "lib/net462/System.IdentityModel.Tokens.Jwt.xml", "lib/net472/System.IdentityModel.Tokens.Jwt.dll", @@ -4107,9 +4140,11 @@ "lib/net6.0/System.IdentityModel.Tokens.Jwt.xml", "lib/net8.0/System.IdentityModel.Tokens.Jwt.dll", "lib/net8.0/System.IdentityModel.Tokens.Jwt.xml", + "lib/net9.0/System.IdentityModel.Tokens.Jwt.dll", + "lib/net9.0/System.IdentityModel.Tokens.Jwt.xml", "lib/netstandard2.0/System.IdentityModel.Tokens.Jwt.dll", "lib/netstandard2.0/System.IdentityModel.Tokens.Jwt.xml", - "system.identitymodel.tokens.jwt.7.7.1.nupkg.sha512", + "system.identitymodel.tokens.jwt.8.0.1.nupkg.sha512", "system.identitymodel.tokens.jwt.nuspec" ] }, @@ -4417,6 +4452,7 @@ "projectFileDependencyGroups": { "net9.0": [ "Dapper >= 2.1.66", + "Microsoft.AspNetCore.Authentication.JwtBearer >= 9.0.9", "Microsoft.AspNetCore.OpenApi >= 9.0.5", "Microsoft.Data.SqlClient >= 6.1.1", "Microsoft.EntityFrameworkCore.Design >= 9.0.9", @@ -4479,6 +4515,10 @@ "target": "Package", "version": "[2.1.66, )" }, + "Microsoft.AspNetCore.Authentication.JwtBearer": { + "target": "Package", + "version": "[9.0.9, )" + }, "Microsoft.AspNetCore.OpenApi": { "target": "Package", "version": "[9.0.5, )" diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2583597..68f2ea3 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4,12 +4,24 @@ import GestionSectores from "./components/GestionSectores"; import GestionComponentes from './components/GestionComponentes'; import Dashboard from './components/Dashboard'; import Navbar from './components/Navbar'; +import { useAuth } from './context/AuthContext'; +import Login from './components/Login'; import './App.css'; export type View = 'equipos' | 'sectores' | 'admin' | 'dashboard'; function App() { const [currentView, setCurrentView] = useState('equipos'); + const { isAuthenticated, isLoading } = useAuth(); + + // Muestra un loader mientras se verifica la sesión + if (isLoading) { + return
Cargando...
; + } + + if (!isAuthenticated) { + return ; + } return ( <> diff --git a/frontend/src/components/Login.tsx b/frontend/src/components/Login.tsx new file mode 100644 index 0000000..3c3c504 --- /dev/null +++ b/frontend/src/components/Login.tsx @@ -0,0 +1,75 @@ +// frontend/src/components/Login.tsx +import React, { useState } from 'react'; +import toast from 'react-hot-toast'; +import { authService } from '../services/apiService'; +import { useAuth } from '../context/AuthContext'; +import styles from './SimpleTable.module.css'; + +const Login = () => { + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [rememberMe, setRememberMe] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const { login } = useAuth(); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + try { + // 2. Pasar el estado del checkbox al servicio + const data = await authService.login(username, password, rememberMe); + // 3. Pasar el token Y el estado del checkbox al contexto + login(data.token, rememberMe); + toast.success('¡Bienvenido!'); + } catch (error) { + toast.error('Usuario o contraseña incorrectos.'); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+

Iniciar Sesión - Inventario IT

+
+ + setUsername(e.target.value)} + className={styles.modalInput} + required + /> + + setPassword(e.target.value)} + className={styles.modalInput} + required + /> +
+ setRememberMe(e.target.checked)} + style={{ marginRight: '0.5rem' }} + /> + +
+
+ +
+
+
+
+ ); +}; + +export default Login; \ No newline at end of file diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx index a9bc8c9..fe73f76 100644 --- a/frontend/src/components/Navbar.tsx +++ b/frontend/src/components/Navbar.tsx @@ -2,6 +2,8 @@ import React from 'react'; import type { View } from '../App'; import ThemeToggle from './ThemeToggle'; +import { useAuth } from '../context/AuthContext'; +import { LogOut } from 'lucide-react'; import '../App.css'; interface NavbarProps { @@ -10,12 +12,13 @@ interface NavbarProps { } const Navbar: React.FC = ({ currentView, setCurrentView }) => { + const { logout } = useAuth(); return (
Inventario IT
-
); }; diff --git a/frontend/src/context/AuthContext.tsx b/frontend/src/context/AuthContext.tsx new file mode 100644 index 0000000..d7ce4af --- /dev/null +++ b/frontend/src/context/AuthContext.tsx @@ -0,0 +1,64 @@ +// frontend/src/context/AuthContext.tsx +import React, { createContext, useState, useContext, useMemo, useEffect } from 'react'; + +interface AuthContextType { + token: string | null; + isAuthenticated: boolean; + isLoading: boolean; + // 1. Modificar la firma de la función login + login: (token: string, rememberMe: boolean) => void; + logout: () => void; +} + +const AuthContext = createContext(undefined); + +export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [token, setToken] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + // 2. Al cargar, buscar el token en localStorage primero, y luego en sessionStorage + const storedToken = localStorage.getItem('authToken') || sessionStorage.getItem('authToken'); + setToken(storedToken); + setIsLoading(false); + }, []); + + // 3. Implementar la nueva lógica de login + const login = (newToken: string, rememberMe: boolean) => { + if (rememberMe) { + // Si el usuario quiere ser recordado, usamos localStorage + localStorage.setItem('authToken', newToken); + } else { + // Si no, usamos sessionStorage + sessionStorage.setItem('authToken', newToken); + } + setToken(newToken); + }; + + // 4. Asegurarnos de que el logout limpie ambos almacenamientos + const logout = () => { + // Asegurarse de limpiar ambos almacenamientos + localStorage.removeItem('authToken'); + sessionStorage.removeItem('authToken'); + setToken(null); + }; + + const isAuthenticated = !!token; + + // 5. Actualizar el valor del contexto + const value = useMemo(() => ({ token, isAuthenticated, isLoading, login, logout }), [token, isLoading]); + + return ( + + {children} + + ); +}; + +export const useAuth = () => { + const context = useContext(AuthContext); + if (context === undefined) { + throw new Error('useAuth debe ser usado dentro de un AuthProvider'); + } + return context; +}; \ No newline at end of file diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 45bf160..3ed9da4 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -5,28 +5,31 @@ import App from './App.tsx' import './index.css' import { Toaster } from 'react-hot-toast' import { ThemeProvider } from './context/ThemeContext'; +import { AuthProvider } from './context/AuthContext'; ReactDOM.createRoot(document.getElementById('root')!).render( - - - + + + - + }} + /> + + , ) \ No newline at end of file diff --git a/frontend/src/services/apiService.ts b/frontend/src/services/apiService.ts index 5cd5f86..d651cdc 100644 --- a/frontend/src/services/apiService.ts +++ b/frontend/src/services/apiService.ts @@ -4,10 +4,37 @@ import type { Equipo, Sector, HistorialEquipo, Usuario, MemoriaRam, DashboardSta const BASE_URL = '/api'; -async function request(url: string, options?: RequestInit): Promise { +// --- FUNCIÓN 'request' --- +async function request(url: string, options: RequestInit = {}): Promise { + // 1. Intentar obtener el token de localStorage primero, si no existe, buscar en sessionStorage. + const token = localStorage.getItem('authToken') || sessionStorage.getItem('authToken'); + + // 2. Añadir el token al encabezado de autorización si existe + const headers = new Headers(options.headers); + if (token) { + headers.append('Authorization', `Bearer ${token}`); + } + options.headers = headers; + const response = await fetch(url, options); + // 3. Manejar errores de autenticación + if (response.status === 401) { + // SOLO recargamos si el error 401 NO viene del endpoint de login. + // Esto es para el caso de un token expirado en una petición a una ruta protegida. + if (!url.includes('/auth/login')) { + localStorage.removeItem('authToken'); + sessionStorage.removeItem('authToken'); + window.location.reload(); + // La recarga previene que el resto del código se ejecute. + // Lanzamos un error para detener la ejecución de esta promesa. + throw new Error('Sesión expirada. Por favor, inicie sesión de nuevo.'); + } + } + if (!response.ok) { + // Para el login, el 401 llegará hasta aquí y lanzará el error + // que será capturado por el componente Login.tsx. const errorData = await response.json().catch(() => ({ message: 'Error en la respuesta del servidor' })); throw new Error(errorData.message || 'Ocurrió un error desconocido'); } @@ -19,6 +46,17 @@ async function request(url: string, options?: RequestInit): Promise { return response.json(); } +// --- SERVICIO PARA AUTENTICACIÓN --- +export const authService = { + // Añadimos el parámetro 'rememberMe' + login: (username: string, password: string, rememberMe: boolean) => + request<{ token: string }>(`${BASE_URL}/auth/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, password, rememberMe }), + }), +}; + // --- Servicio para la gestión de Sectores --- export const sectorService = { getAll: () => request(`${BASE_URL}/sectores`),