Fase 3:
- 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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user