Fase 2: Creatción de la UI (React + Vite). Implementación de Log In reemplazando texto plano. Y creación de tool para migrar contraseñas.

This commit is contained in:
2025-05-05 15:49:01 -03:00
parent 9b1de95404
commit da7b544372
81 changed files with 12260 additions and 99 deletions

7
Frontend/src/App.tsx Normal file
View File

@@ -0,0 +1,7 @@
import AppRoutes from './routes/AppRoutes';
function App() {
return <AppRoutes />;
}
export default App;

View File

@@ -0,0 +1,73 @@
import React, { createContext, useState, useContext, useEffect } from 'react';
import type { ReactNode } from 'react'; // Importar como tipo
import type { LoginResponseDto } from '../models/dtos/LoginResponseDto';
interface AuthContextType {
isAuthenticated: boolean;
user: LoginResponseDto | null;
token: string | null;
isLoading: boolean; // Para saber si aún está verificando el token inicial
login: (userData: LoginResponseDto) => void;
logout: () => void;
}
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 [token, setToken] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true); // Empieza cargando
// Efecto para verificar token al cargar la app
useEffect(() => {
const storedToken = localStorage.getItem('authToken');
const storedUser = localStorage.getItem('authUser'); // Guardamos el usuario también
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
}
}
setIsLoading(false); // Termina la carga inicial
}, []);
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 logout = () => {
localStorage.removeItem('authToken');
localStorage.removeItem('authUser');
setToken(null);
setUser(null);
setIsAuthenticated(false);
};
return (
<AuthContext.Provider value={{ isAuthenticated, user, token, isLoading, login, logout }}>
{children}
</AuthContext.Provider>
);
};
// Hook personalizado para usar el contexto fácilmente
export const useAuth = (): AuthContextType => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};

View File

@@ -0,0 +1,48 @@
import React from 'react';
import type { ReactNode } from 'react'; // Importar como tipo
import { Box, AppBar, Toolbar, Typography, Button } from '@mui/material';
import { useAuth } from '../contexts/AuthContext';
interface MainLayoutProps {
children: ReactNode; // Para renderizar las páginas hijas
}
const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
const { user, logout } = useAuth();
return (
<Box sx={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}>
<AppBar position="static">
<Toolbar>
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
Gestión Integral
</Typography>
{user && <Typography sx={{ mr: 2 }}>Hola, {user.Username}</Typography> }
<Button color="inherit" onClick={logout}>Cerrar Sesión</Button>
</Toolbar>
{/* Aquí iría el MaterialTabControl o similar para la navegación principal */}
</AppBar>
<Box
component="main"
sx={{
flexGrow: 1,
p: 3, // Padding
// Puedes añadir color de fondo si lo deseas
// backgroundColor: (theme) => theme.palette.background.default,
}}
>
{/* El contenido de la página actual se renderizará aquí */}
{children}
</Box>
{/* Aquí podría ir un Footer o StatusStrip */}
<Box component="footer" sx={{ p: 1, mt: 'auto', backgroundColor: 'primary.dark', color: 'white', textAlign: 'center' }}>
<Typography variant="body2">
{/* Replicar info del StatusStrip original */}
Usuario: {user?.Username} | Acceso: {user?.EsSuperAdmin ? 'Super Admin' : 'Perfil...'} | Versión: {/** Obtener versión **/}
</Typography>
</Box>
</Box>
);
};
export default MainLayout;

18
Frontend/src/main.tsx Normal file
View File

@@ -0,0 +1,18 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
import { ThemeProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import theme from './theme/theme';
import { AuthProvider } from './contexts/AuthContext'; // Importar AuthProvider
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<ThemeProvider theme={theme}>
<AuthProvider> {/* Envolver con AuthProvider */}
<CssBaseline />
<App />
</AuthProvider> {/* Cerrar AuthProvider */}
</ThemeProvider>
</React.StrictMode>,
);

View File

@@ -0,0 +1,5 @@
// src/models/dtos/LoginRequestDto.ts
export interface LoginRequestDto {
Username: string; // Coincide con las propiedades C#
Password: string;
}

View File

@@ -0,0 +1,10 @@
// src/models/dtos/LoginResponseDto.ts
export interface LoginResponseDto {
Token: string;
UserId: number;
Username: string;
NombreCompleto: string;
EsSuperAdmin: boolean;
DebeCambiarClave: boolean;
// Añade otros campos si los definiste en el DTO C#
}

View File

@@ -0,0 +1,23 @@
import React from 'react';
import { Typography, Container } from '@mui/material';
// import { useLocation } from 'react-router-dom'; // Para obtener el estado 'firstLogin'
const ChangePasswordPage: React.FC = () => {
// const location = useLocation();
// const isFirstLogin = location.state?.firstLogin;
return (
<Container>
<Typography variant="h4" component="h1" gutterBottom>
Cambiar Contraseña
</Typography>
{/* {isFirstLogin && <Alert severity="warning">Debes cambiar tu contraseña inicial.</Alert>} */}
{/* Aquí irá el formulario de cambio de contraseña */}
<Typography variant="body1">
Formulario de cambio de contraseña irá aquí...
</Typography>
</Container>
);
};
export default ChangePasswordPage;

View File

@@ -0,0 +1,17 @@
import React from 'react';
import { Typography, Container } from '@mui/material';
const HomePage: React.FC = () => {
return (
<Container>
<Typography variant="h4" component="h1" gutterBottom>
Bienvenido al Sistema
</Typography>
<Typography variant="body1">
Seleccione una opción del menú principal para comenzar.
</Typography>
</Container>
);
};
export default HomePage;

View File

@@ -0,0 +1,112 @@
import React, { useState } from 'react';
import axios from 'axios'; // Importar axios
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
import apiClient from '../services/apiClient'; // Nuestro cliente axios
import type { LoginRequestDto } from '../models/dtos/LoginRequestDto'; // Usar type
import type { LoginResponseDto } from '../models/dtos/LoginResponseDto'; // Usar type
// Importaciones de Material UI
import { Container, TextField, Button, Typography, Box, Alert } from '@mui/material';
const LoginPage: React.FC = () => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const { login } = useAuth();
const navigate = useNavigate();
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setError(null);
setLoading(true);
const loginData: LoginRequestDto = { Username: username, Password: password };
try {
const response = await apiClient.post<LoginResponseDto>('/auth/login', loginData);
login(response.data); // Guardar token y estado de usuario en el contexto
// TODO: Verificar si response.data.DebeCambiarClave es true y redirigir
// a '/change-password' si es necesario.
// if (response.data.DebeCambiarClave) {
// navigate('/change-password', { state: { firstLogin: true } }); // Pasar estado si es necesario
// } else {
navigate('/'); // Redirigir a la página principal
// }
} catch (err: any) {
console.error("Login error:", err);
if (axios.isAxiosError(err) && err.response) {
// Intenta obtener el mensaje de error de la API, si no, usa uno genérico
setError(err.response.data?.message || 'Error al iniciar sesión. Verifique sus credenciales.');
} else {
setError('Ocurrió un error inesperado.');
}
} finally {
setLoading(false);
}
};
return (
<Container component="main" maxWidth="xs">
<Box
sx={{
marginTop: 8,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<Typography component="h1" variant="h5">
Iniciar Sesión
</Typography>
<Box component="form" onSubmit={handleSubmit} noValidate sx={{ mt: 1 }}>
<TextField
margin="normal"
required
fullWidth
id="username"
label="Usuario"
name="username"
autoComplete="username"
autoFocus
value={username}
onChange={(e) => setUsername(e.target.value)}
disabled={loading}
/>
<TextField
margin="normal"
required
fullWidth
name="password"
label="Contraseña"
type="password"
id="password"
autoComplete="current-password"
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={loading}
/>
{error && (
<Alert severity="error" sx={{ mt: 2, width: '100%' }}>
{error}
</Alert>
)}
<Button
type="submit"
fullWidth
variant="contained"
sx={{ mt: 3, mb: 2 }}
disabled={loading}
>
{loading ? 'Ingresando...' : 'Ingresar'}
</Button>
</Box>
</Box>
</Container>
);
};
export default LoginPage;

View File

@@ -0,0 +1,63 @@
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import LoginPage from '../pages/LoginPage';
import HomePage from '../pages/HomePage'; // Crearemos esta página simple
import { useAuth } from '../contexts/AuthContext';
import ChangePasswordPage from '../pages/ChangePasswordPage'; // Crearemos esta
import MainLayout from '../layouts/MainLayout'; // Crearemos este
// Componente para proteger rutas
const ProtectedRoute: React.FC<{ children: JSX.Element }> = ({ children }) => {
const { isAuthenticated, isLoading } = useAuth();
if (isLoading) {
// Muestra algo mientras verifica el token (ej: un spinner)
return <div>Cargando...</div>;
}
return isAuthenticated ? children : <Navigate to="/login" replace />;
};
// Componente para rutas públicas (redirige si ya está logueado)
const PublicRoute: React.FC<{ children: JSX.Element }> = ({ children }) => {
const { isAuthenticated, isLoading } = useAuth();
if (isLoading) {
return <div>Cargando...</div>;
}
return !isAuthenticated ? children : <Navigate to="/" replace />;
};
const AppRoutes = () => {
return (
<BrowserRouter>
<Routes>
{/* Rutas Públicas */}
<Route path="/login" element={<PublicRoute><LoginPage /></PublicRoute>} />
<Route path="/change-password" element={<ProtectedRoute><ChangePasswordPage /></ProtectedRoute>} /> {/* Asumimos que se accede logueado */}
{/* Rutas Protegidas dentro del Layout Principal */}
<Route
path="/*" // Captura todas las demás rutas
element={
<ProtectedRoute>
<MainLayout> {/* Layout que tendrá la navegación principal */}
{/* Aquí irán las rutas de los módulos */}
<Routes>
<Route index element={<HomePage />} /> {/* Página por defecto al loguearse */}
{/* <Route path="/usuarios" element={<GestionUsuariosPage />} /> */}
{/* <Route path="/zonas" element={<GestionZonasPage />} /> */}
{/* ... otras rutas de módulos ... */}
<Route path="*" element={<Navigate to="/" replace />} /> {/* Redirige rutas no encontradas al home */}
</Routes>
</MainLayout>
</ProtectedRoute>
}
/>
</Routes>
</BrowserRouter>
);
};
export default AppRoutes;

View File

@@ -0,0 +1,30 @@
import axios from 'axios';
// Obtén la URL base de tu API desde variables de entorno o configúrala aquí
// Asegúrate que coincida con la URL donde corre tu API ASP.NET Core
const API_BASE_URL = 'http://localhost:5183/api'; // ¡AJUSTA EL PUERTO SI ES DIFERENTE! (Verifica la salida de 'dotnet run')
const apiClient = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
},
});
// Interceptor para añadir el token JWT a las peticiones (si existe)
apiClient.interceptors.request.use(
(config) => {
const token = localStorage.getItem('authToken'); // O donde guardes el token
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Puedes añadir interceptores de respuesta para manejar errores globales (ej: 401 Unauthorized)
export default apiClient;

View File

@@ -0,0 +1,36 @@
import { createTheme } from '@mui/material/styles';
import { esES } from '@mui/material/locale'; // Importar localización español
// Paleta similar a la que definiste para MaterialSkin
const theme = createTheme({
palette: {
primary: {
main: '#607d8b', // BlueGrey 500
light: '#8eacbb',
dark: '#34515e', // Un poco más oscuro que BlueGrey 700
},
secondary: {
main: '#455a64', // BlueGrey 700
light: '#718792',
dark: '#1c313a', // BlueGrey 900
},
background: {
default: '#eceff1', // BlueGrey 50 (similar a LightBlue50)
paper: '#ffffff', // Blanco para superficies como cards
},
// El Accent de MaterialSkin es más difícil de mapear directamente,
// puedes usar 'secondary' o definir colores personalizados si es necesario.
// Usaremos secondary por ahora.
// text: { // MUI infiere esto, pero puedes forzar blanco si es necesario
// primary: '#ffffff',
// secondary: 'rgba(255, 255, 255, 0.7)',
// },
},
typography: {
// Puedes personalizar fuentes aquí si lo deseas
fontFamily: 'Roboto, Arial, sans-serif',
},
// Añadir localización
}, esES); // Pasar el objeto de localización
export default theme;

1
Frontend/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />