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.
This commit is contained in:
75
frontend/src/components/Login.tsx
Normal file
75
frontend/src/components/Login.tsx
Normal file
@@ -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 (
|
||||
<div className={styles.modalOverlay} style={{ animation: 'none' }}>
|
||||
<div className={styles.modal} style={{ animation: 'none' }}>
|
||||
<h3>Iniciar Sesión - Inventario IT</h3>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<label>Usuario</label>
|
||||
<input
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className={styles.modalInput}
|
||||
required
|
||||
/>
|
||||
<label style={{ marginTop: '1rem' }}>Contraseña</label>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className={styles.modalInput}
|
||||
required
|
||||
/>
|
||||
<div style={{ marginTop: '1rem', display: 'flex', alignItems: 'center' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="rememberMe"
|
||||
checked={rememberMe}
|
||||
onChange={(e) => setRememberMe(e.target.checked)}
|
||||
style={{ marginRight: '0.5rem' }}
|
||||
/>
|
||||
<label htmlFor="rememberMe" style={{ marginBottom: 0, fontWeight: 'normal' }}>
|
||||
Mantener sesión iniciada
|
||||
</label>
|
||||
</div>
|
||||
<div className={styles.modalActions} style={{ marginTop: '1.5rem' }}>
|
||||
<button type="submit" className={`${styles.btn} ${styles.btnPrimary}`} disabled={isLoading || !username || !password}>
|
||||
{isLoading ? 'Ingresando...' : 'Ingresar'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
@@ -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<NavbarProps> = ({ currentView, setCurrentView }) => {
|
||||
const { logout } = useAuth();
|
||||
return (
|
||||
<header className="navbar">
|
||||
<div className="app-title">
|
||||
Inventario IT
|
||||
</div>
|
||||
<nav className="nav-links">
|
||||
<nav className="nav-links">
|
||||
<button
|
||||
className={`nav-link ${currentView === 'equipos' ? 'nav-link-active' : ''}`}
|
||||
onClick={() => setCurrentView('equipos')}
|
||||
@@ -40,11 +43,17 @@ const Navbar: React.FC<NavbarProps> = ({ currentView, setCurrentView }) => {
|
||||
>
|
||||
Dashboard
|
||||
</button>
|
||||
<div style={{ padding: '0.25rem' }}>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginLeft: '1rem' }}>
|
||||
<ThemeToggle />
|
||||
<button
|
||||
onClick={logout}
|
||||
className="theme-toggle-button"
|
||||
title="Cerrar sesión"
|
||||
>
|
||||
<LogOut size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user