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:
24
Frontend/.gitignore
vendored
Normal file
24
Frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
54
Frontend/README.md
Normal file
54
Frontend/README.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||
|
||||
```js
|
||||
export default tseslint.config({
|
||||
extends: [
|
||||
// Remove ...tseslint.configs.recommended and replace with this
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
// Alternatively, use this for stricter rules
|
||||
...tseslint.configs.strictTypeChecked,
|
||||
// Optionally, add this for stylistic rules
|
||||
...tseslint.configs.stylisticTypeChecked,
|
||||
],
|
||||
languageOptions: {
|
||||
// other options...
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import reactX from 'eslint-plugin-react-x'
|
||||
import reactDom from 'eslint-plugin-react-dom'
|
||||
|
||||
export default tseslint.config({
|
||||
plugins: {
|
||||
// Add the react-x and react-dom plugins
|
||||
'react-x': reactX,
|
||||
'react-dom': reactDom,
|
||||
},
|
||||
rules: {
|
||||
// other rules...
|
||||
// Enable its recommended typescript rules
|
||||
...reactX.configs['recommended-typescript'].rules,
|
||||
...reactDom.configs.recommended.rules,
|
||||
},
|
||||
})
|
||||
```
|
||||
28
Frontend/eslint.config.js
Normal file
28
Frontend/eslint.config.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
||||
13
Frontend/index.html
Normal file
13
Frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/eldia.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Sistema de Gestión El Día</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
5114
Frontend/package-lock.json
generated
Normal file
5114
Frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
35
Frontend/package.json
Normal file
35
Frontend/package.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@mui/icons-material": "^7.0.2",
|
||||
"@mui/material": "^7.0.2",
|
||||
"axios": "^1.9.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-router-dom": "^7.5.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.25.0",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"globals": "^16.0.0",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.30.1",
|
||||
"vite": "^6.3.5"
|
||||
}
|
||||
}
|
||||
6
Frontend/public/eldia.svg
Normal file
6
Frontend/public/eldia.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="89" height="69">
|
||||
<path d="M0 0 C29.37 0 58.74 0 89 0 C89 22.77 89 45.54 89 69 C59.63 69 30.26 69 0 69 C0 46.23 0 23.46 0 0 Z " fill="#008FBD" transform="translate(0,0)"/>
|
||||
<path d="M0 0 C3.3 0 6.6 0 10 0 C13.04822999 3.04822999 12.29337257 6.08805307 12.32226562 10.25390625 C12.31904297 11.32511719 12.31582031 12.39632812 12.3125 13.5 C12.32861328 14.57121094 12.34472656 15.64242187 12.36132812 16.74609375 C12.36197266 17.76832031 12.36261719 18.79054688 12.36328125 19.84375 C12.36775269 21.25430664 12.36775269 21.25430664 12.37231445 22.69335938 C12.1880188 23.83514648 12.1880188 23.83514648 12 25 C9 27 9 27 0 27 C0 18.09 0 9.18 0 0 Z " fill="#000000" transform="translate(0,21)"/>
|
||||
<path d="M0 0 C5.61 0 11.22 0 17 0 C17 3.3 17 6.6 17 10 C25.58 10 34.16 10 43 10 C43 10.99 43 11.98 43 13 C34.42 13 25.84 13 17 13 C17 17.29 17 21.58 17 26 C11.39 26 5.78 26 0 26 C0 24.68 0 23.36 0 22 C4.62 22 9.24 22 14 22 C14 16.06 14 10.12 14 4 C9.38 4 4.76 4 0 4 C0 2.68 0 1.36 0 0 Z " fill="#000000" transform="translate(46,21)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
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" />
|
||||
27
Frontend/tsconfig.app.json
Normal file
27
Frontend/tsconfig.app.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
7
Frontend/tsconfig.json
Normal file
7
Frontend/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
25
Frontend/tsconfig.node.json
Normal file
25
Frontend/tsconfig.node.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
7
Frontend/vite.config.ts
Normal file
7
Frontend/vite.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
Reference in New Issue
Block a user