- Backend API:
Autenticación y autorización básicas con JWT implementadas.
Cambio de contraseña funcional.
Módulo "Tipos de Pago" (CRUD completo) implementado en el backend (Controlador, Servicio, Repositorio) usando Dapper, transacciones y con lógica de historial.
Se incluyen permisos en el token JWT.
- Frontend React:
Estructura base con Vite, TypeScript, MUI.
Contexto de autenticación (AuthContext) que maneja el estado del usuario y el token.
Página de Login.
Modal de Cambio de Contraseña (forzado y opcional).
Hook usePermissions para verificar permisos.
Página GestionarTiposPagoPage con tabla, paginación, filtro, modal para crear/editar, y menú de acciones, respetando permisos.
Layout principal (MainLayout) con navegación por Tabs (funcionalidad básica de navegación).
Estructura de enrutamiento (AppRoutes) que maneja rutas públicas, protegidas y anidadas para módulos.
This commit is contained in:
2025-05-07 13:41:18 -03:00
parent da7b544372
commit 5c4b961073
49 changed files with 2552 additions and 491 deletions

View File

@@ -1,13 +1,41 @@
import React, { createContext, useState, useContext, useEffect } from 'react';
import type { ReactNode } from 'react'; // Importar como tipo
import React, { createContext, useState, useContext, type ReactNode, useEffect } from 'react';
import type { LoginResponseDto } from '../models/dtos/LoginResponseDto';
import { jwtDecode } from 'jwt-decode';
// Interfaz para los datos del usuario que guardaremos en el contexto
export interface UserContextData {
userId: number;
username: string;
nombreCompleto: string;
esSuperAdmin: boolean;
debeCambiarClave: boolean;
idPerfil: number;
permissions: string[]; // Guardamos los codAcc
}
// Interfaz para el payload decodificado del JWT
interface DecodedJwtPayload {
sub: string; // User ID (viene como string)
name: string; // Username
given_name?: string; // Nombre (estándar, pero verifica tu token)
family_name?: string; // Apellido (estándar, pero verifica tu token)
role: string | string[]; // Puede ser uno o varios roles
idPerfil: string; // (viene como string)
debeCambiarClave: string; // (viene como string "True" o "False")
permission?: string | string[]; // Nuestros claims de permiso (codAcc)
[key: string]: any; // Para otros claims
}
interface AuthContextType {
isAuthenticated: boolean;
user: LoginResponseDto | null;
user: UserContextData | null; // Usar el tipo extendido
token: string | null;
isLoading: boolean; // Para saber si aún está verificando el token inicial
login: (userData: LoginResponseDto) => void;
isLoading: boolean;
showForcedPasswordChangeModal: boolean;
isPasswordChangeForced: boolean;
setShowForcedPasswordChangeModal: (show: boolean) => void;
passwordChangeCompleted: () => void;
login: (apiLoginResponse: LoginResponseDto) => void; // Recibe el DTO de la API
logout: () => void;
}
@@ -15,37 +43,68 @@ const AuthContext = createContext<AuthContextType | undefined>(undefined);
export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
const [user, setUser] = useState<LoginResponseDto | null>(null);
const [user, setUser] = useState<UserContextData | null>(null);
const [token, setToken] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true); // Empieza cargando
const [isLoading, setIsLoading] = useState<boolean>(true);
const [showForcedPasswordChangeModal, setShowForcedPasswordChangeModal] = useState<boolean>(false);
const [isPasswordChangeForced, setIsPasswordChangeForced] = useState<boolean>(false);
// Efecto para verificar token al cargar la app
useEffect(() => {
const storedToken = localStorage.getItem('authToken');
const storedUser = localStorage.getItem('authUser'); // Guardamos el usuario también
const processTokenAndSetUser = (jwtToken: string) => {
try {
const decodedToken = jwtDecode<DecodedJwtPayload>(jwtToken);
if (storedToken && storedUser) {
try {
// Aquí podrías añadir lógica para validar si el token aún es válido (ej: decodificarlo)
// Por ahora, simplemente asumimos que si está, es válido.
const parsedUser: LoginResponseDto = JSON.parse(storedUser);
setToken(storedToken);
setUser(parsedUser);
setIsAuthenticated(true);
} catch (error) {
console.error("Error parsing stored user data", error);
logout(); // Limpia si hay error al parsear
// Verificar expiración (opcional, pero buena práctica aquí también)
const currentTime = Date.now() / 1000;
if (decodedToken.exp && decodedToken.exp < currentTime) {
console.warn("Token expirado al procesar.");
logout(); // Llama a logout que limpia todo
return;
}
let permissions: string[] = [];
if (decodedToken.permission) {
permissions = Array.isArray(decodedToken.permission) ? decodedToken.permission : [decodedToken.permission];
}
const userForContext: UserContextData = {
userId: parseInt(decodedToken.sub, 10),
username: decodedToken.name,
nombreCompleto: `${decodedToken.given_name || ''} ${decodedToken.family_name || ''}`.trim(),
esSuperAdmin: decodedToken.role === "SuperAdmin" || (Array.isArray(decodedToken.role) && decodedToken.role.includes("SuperAdmin")),
debeCambiarClave: decodedToken.debeCambiarClave?.toLowerCase() === 'true',
idPerfil: decodedToken.idPerfil ? parseInt(decodedToken.idPerfil, 10) : 0,
permissions: permissions,
};
setToken(jwtToken);
setUser(userForContext);
setIsAuthenticated(true);
localStorage.setItem('authToken', jwtToken);
localStorage.setItem('authUser', JSON.stringify(userForContext)); // Guardar el usuario procesado
// Lógica para el modal de cambio de clave
if (userForContext.debeCambiarClave) {
setShowForcedPasswordChangeModal(true);
setIsPasswordChangeForced(true);
}
} catch (error) {
console.error("Error al decodificar o procesar token:", error);
logout(); // Limpiar estado si el token es inválido
}
setIsLoading(false); // Termina la carga inicial
};
useEffect(() => {
setIsLoading(true);
const storedToken = localStorage.getItem('authToken');
if (storedToken) {
processTokenAndSetUser(storedToken);
}
setIsLoading(false);
}, []);
const login = (userData: LoginResponseDto) => {
localStorage.setItem('authToken', userData.Token);
localStorage.setItem('authUser', JSON.stringify(userData)); // Guardar datos de usuario
setToken(userData.Token);
setUser(userData);
setIsAuthenticated(true);
const login = (apiLoginResponse: LoginResponseDto) => {
processTokenAndSetUser(apiLoginResponse.token); // Procesar el token recibido
};
const logout = () => {
@@ -54,16 +113,36 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
setToken(null);
setUser(null);
setIsAuthenticated(false);
setShowForcedPasswordChangeModal(false);
setIsPasswordChangeForced(false);
};
const passwordChangeCompleted = () => {
setShowForcedPasswordChangeModal(false);
setIsPasswordChangeForced(false);
// Importante: Si el cambio de clave afecta el claim "debeCambiarClave" en el token,
// idealmente el backend debería devolver un *nuevo token* después del cambio de clave.
// Si no lo hace, el token actual aún dirá que debe cambiar clave.
// Una solución simple es actualizar el estado local del usuario:
if (user) {
const updatedUser = { ...user, debeCambiarClave: false };
setUser(updatedUser);
localStorage.setItem('authUser', JSON.stringify(updatedUser));
}
};
return (
<AuthContext.Provider value={{ isAuthenticated, user, token, isLoading, login, logout }}>
<AuthContext.Provider value={{
isAuthenticated, user, token, isLoading,
showForcedPasswordChangeModal, isPasswordChangeForced,
setShowForcedPasswordChangeModal, passwordChangeCompleted,
login, logout
}}>
{children}
</AuthContext.Provider>
);
};
// Hook personalizado para usar el contexto fácilmente
export const useAuth = (): AuthContextType => {
const context = useContext(AuthContext);
if (context === undefined) {