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:
7
Frontend/src/App.tsx
Normal file
7
Frontend/src/App.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import AppRoutes from './routes/AppRoutes';
|
||||
|
||||
function App() {
|
||||
return <AppRoutes />;
|
||||
}
|
||||
|
||||
export default App;
|
||||
73
Frontend/src/contexts/AuthContext.tsx
Normal file
73
Frontend/src/contexts/AuthContext.tsx
Normal 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;
|
||||
};
|
||||
48
Frontend/src/layouts/MainLayout.tsx
Normal file
48
Frontend/src/layouts/MainLayout.tsx
Normal 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
18
Frontend/src/main.tsx
Normal 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>,
|
||||
);
|
||||
5
Frontend/src/models/dtos/LoginRequestDto.ts
Normal file
5
Frontend/src/models/dtos/LoginRequestDto.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
// src/models/dtos/LoginRequestDto.ts
|
||||
export interface LoginRequestDto {
|
||||
Username: string; // Coincide con las propiedades C#
|
||||
Password: string;
|
||||
}
|
||||
10
Frontend/src/models/dtos/LoginResponseDto.ts
Normal file
10
Frontend/src/models/dtos/LoginResponseDto.ts
Normal 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#
|
||||
}
|
||||
23
Frontend/src/pages/ChangePasswordPage.tsx
Normal file
23
Frontend/src/pages/ChangePasswordPage.tsx
Normal 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;
|
||||
17
Frontend/src/pages/HomePage.tsx
Normal file
17
Frontend/src/pages/HomePage.tsx
Normal 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;
|
||||
112
Frontend/src/pages/LoginPage.tsx
Normal file
112
Frontend/src/pages/LoginPage.tsx
Normal 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;
|
||||
63
Frontend/src/routes/AppRoutes.tsx
Normal file
63
Frontend/src/routes/AppRoutes.tsx
Normal 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;
|
||||
30
Frontend/src/services/apiClient.ts
Normal file
30
Frontend/src/services/apiClient.ts
Normal 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;
|
||||
36
Frontend/src/theme/theme.ts
Normal file
36
Frontend/src/theme/theme.ts
Normal 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
1
Frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
Reference in New Issue
Block a user