diff --git a/cosiap_frontend/src/App.jsx b/cosiap_frontend/src/App.jsx index c35512a59d27fbf65b4a4b189bc017f2ace66a51..9c0fc17ddcb0cfbcc3aa44b23d9571de3386ed0a 100644 --- a/cosiap_frontend/src/App.jsx +++ b/cosiap_frontend/src/App.jsx @@ -1,15 +1,26 @@ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import {PaginaHead} from "@/components/common/utility/PaginaHead"; import {Autenticador} from "@/components/common/utility/Autenticador"; -import {LoginRequiredRoutes} from "@/components/common/utility/LoginRequiredRoutes" +import { useAutenticacion } from '@/components/common/utility/Autenticador'; + +import {LoginRequiredRoute, IsAdminRequiredRoute, IsLogged} from "@/components/common/utility/LoginRequiredRoute" import "./App.css"; import PageLoader from '@/components/common/ui/PageLoader'; -import { Login } from './components/users/Login/Login'; -import Register from './components/users/Register/Register'; -import ResetPassword from './components/users/Password/ResetPassword'; -import Inicio from "./components/Inicio"; + +// Importaciones de componentes de autenticacion +import { Login } from '@/components/users/Login/Login'; +import Register from '@/components/users/Register/Register'; +import ResetPassword from '@/components/users/Password/ResetPassword'; +import NoAutorizado from '@/components/users/NoAutorizado'; +import PageNotFound from '@/components/users/PageNotFound'; + +import Inicio from "@/components/Inicio"; import { useState } from 'react'; -import LayoutBaseAuthenticator from './components/common/layouts/LayoutBaseAuthenticator'; +// Importaciones de layoutsBase +import LayoutBaseAuthenticator from '@/components/common/layouts/LayoutBaseAuthenticator'; +import LayoutBaseNavigation from '@/components/common/layouts/LayoutBaseNavigation'; + + function App() { @@ -27,28 +38,56 @@ function App() { viewPageLoader && } - - - {/* Rutas públicas */} - - {/* Componente del layout */} - }> - {/* Componentes hijos del layout autenticador */} - } /> - } /> - } /> - } /> - - - {/* Rutas protegidas */} - }> - } /> - - + + ); } -export default App; +function RoutesApp( {setViewPageLoader} ){ + const {token} = useAutenticacion(); + + return ( + + } /> + {/* Rutas públicas */} + {/* Componente del layout */} + }> + }> + {/* Componentes hijos del layout autenticador */} + } /> + } /> + } /> + + + {/* Rutas protegidas */} + }> + {/* Componentes del layout de navegación */} + }> + {/* Componentes hijos del layout autenticador */} + + {/* Cualquier usuario puede acceder a estas url */} + } /> + } /> + } /> + } /> + } /> + } /> + {/* Solo administradores pueden acceder a estas url */} + }> + } /> + } /> + + + + } /> + + } /> + + + ); +} + +export default App; \ No newline at end of file diff --git a/cosiap_frontend/src/api.js b/cosiap_frontend/src/api.js index 26b0c7236b97d6c0f7b224a4515191648975a090..9a2a5ed1c05a4c190988509430d91bfce494316f 100644 --- a/cosiap_frontend/src/api.js +++ b/cosiap_frontend/src/api.js @@ -1,4 +1,5 @@ import axios from "axios"; + axios.defaults.withCredentials = true; const apiUrl = "http://localhost:8000"; @@ -6,7 +7,7 @@ const apiUrl = "http://localhost:8000"; // Configuración de Axios const ax = axios.create({ - baseURL: apiUrl, + baseURL: apiUrl, }); // Interceptor de solicitud @@ -64,6 +65,11 @@ const api = { token: { login: (data) => ax.post('api/usuarios/token/',data), refresh: () => ax.post('api/usuarios/token/refresh/'), + logout: () => ax.get('api/usuarios/logout/') + }, + + admin: { + is_admin: () => ax.get('api/usuarios/user-is-admin/') }, //Endpoints del submodulo solicitantes diff --git a/cosiap_frontend/src/components/common/layouts/LayoutBaseNavigation.jsx b/cosiap_frontend/src/components/common/layouts/LayoutBaseNavigation.jsx index 1c1cae11ce353f3e08178810849368277d8c4988..da255dd87a27500fd9e67a11aac9ca431a4fae8c 100644 --- a/cosiap_frontend/src/components/common/layouts/LayoutBaseNavigation.jsx +++ b/cosiap_frontend/src/components/common/layouts/LayoutBaseNavigation.jsx @@ -5,7 +5,6 @@ import Sidebar from "@/components/common/layouts/LayoutsNavigation/Sidebar/Sideb import SettingsToggle from "@/components/common/layouts/LayoutsNavigation/SettingsToggle"; import Settings from "@/components/common/layouts/LayoutsNavigation/Settings"; - const linksItemsSolicitante = [ { nameIcon: 'library_books', @@ -47,7 +46,7 @@ const linksItemsSolicitante = [ export default function LayoutBaseNavigation() { const [viewMenu, setViewMenu] = useState(false); - const [selectedNav, setSelectedNav] = useState("vertical"); + const [selectedNav, setSelectedNav] = useState('vertical'); const [viewSettings, setViewSettings] = useState(false); const [links, setLinks] = useState([...linksItemsSolicitante]); @@ -61,20 +60,28 @@ export default function LayoutBaseNavigation() { setSectionURL(location.pathname.split('/').filter(Boolean).pop()); }, [location]); + useEffect(() => { + const savedNavStyle = localStorage.getItem('selectedNav'); + if (savedNavStyle) { + setSelectedNav(savedNavStyle); + } + }, []); + + useEffect(() => { + localStorage.setItem('selectedNav', selectedNav); + }, [selectedNav]); + useEffect(() => { const updatedLinks = links.map(link => { - // Verificación directa basada en 'nameItem' y 'navigate' const isSelected = link.nameItem.toLowerCase() === sectionURL.toLowerCase() || link.navigate?.split('/').pop().toLowerCase() === sectionURL.toLowerCase(); - // Actualizar submenú si existe const updatedSubMenu = link.subMenu?.map(sub => ({ ...sub, isSelected: sub.nameItem.toLowerCase() === sectionURL.toLowerCase() || sub.navigate?.split('/').pop().toLowerCase() === sectionURL.toLowerCase() })); - // Marcar padre si algún subelemento está seleccionado const isParentSelected = updatedSubMenu?.some(sub => sub.isSelected); return { diff --git a/cosiap_frontend/src/components/common/layouts/LayoutsNavigation/Navbar/MobileMenu.jsx b/cosiap_frontend/src/components/common/layouts/LayoutsNavigation/Navbar/MobileMenu.jsx index c0f41073d52c57ce0f3447a7c71ca08aaff26b92..78089a0bbd16d2417f46a0e6a51f9975977a1486 100644 --- a/cosiap_frontend/src/components/common/layouts/LayoutsNavigation/Navbar/MobileMenu.jsx +++ b/cosiap_frontend/src/components/common/layouts/LayoutsNavigation/Navbar/MobileMenu.jsx @@ -1,7 +1,9 @@ import { useState } from "react"; import { useNavigate } from "react-router-dom"; +import { useAutenticacion } from '@/components/common/utility/Autenticador'; export default function MobileMenu({ menuRef, linksItems }) { + const {cerrarSesion} = useAutenticacion(); const navigate = useNavigate(); // useState para manejar la visibilidad del submenú de "Ayuda" @@ -60,7 +62,9 @@ export default function MobileMenu({ menuRef, linksItems }) { ) : '' // Si no hay linksItems, no se muestra nada } {/* Enlace de cerrar sesión */} - + logout {/* Icono del ítem */} diff --git a/cosiap_frontend/src/components/common/layouts/LayoutsNavigation/Navbar/Navbar.jsx b/cosiap_frontend/src/components/common/layouts/LayoutsNavigation/Navbar/Navbar.jsx index 7af25c2b348c8d87c936e262b36e2edab4f1dd24..7350992335176f534631fbbd5367f03ffc1b9d6a 100644 --- a/cosiap_frontend/src/components/common/layouts/LayoutsNavigation/Navbar/Navbar.jsx +++ b/cosiap_frontend/src/components/common/layouts/LayoutsNavigation/Navbar/Navbar.jsx @@ -72,7 +72,7 @@ export default function Navbar({ linksItems, viewMenu, setViewMenu }) { {/* Contenedor para los toggles del menú móvil y notificaciones en pantallas pequeñas */} -
+
{/* Toggle de notificaciones para móviles */} logout diff --git a/cosiap_frontend/src/components/common/layouts/LayoutsNavigation/NotificationToggle.jsx b/cosiap_frontend/src/components/common/layouts/LayoutsNavigation/NotificationToggle.jsx index 33a002e5a0a70b02f38013e334dd2da7d18790dc..29acd0746e576d56b2b96295290368ad1c7d35e0 100644 --- a/cosiap_frontend/src/components/common/layouts/LayoutsNavigation/NotificationToggle.jsx +++ b/cosiap_frontend/src/components/common/layouts/LayoutsNavigation/NotificationToggle.jsx @@ -5,7 +5,7 @@ export default function NotificationToggle( {buttonNotificationsRef, viewNotific {viewNotifications ? 'notifications_active' : 'notifications'} - + diff --git a/cosiap_frontend/src/components/common/layouts/LayoutsNavigation/Sidebar/MobileMenu.jsx b/cosiap_frontend/src/components/common/layouts/LayoutsNavigation/Sidebar/MobileMenu.jsx index 15e7b691b39bf6bd5118ded00b6b4f1f692865df..426cc85a803d556c174243e1cde1cf17560db163 100644 --- a/cosiap_frontend/src/components/common/layouts/LayoutsNavigation/Sidebar/MobileMenu.jsx +++ b/cosiap_frontend/src/components/common/layouts/LayoutsNavigation/Sidebar/MobileMenu.jsx @@ -1,8 +1,10 @@ import { useState, useEffect, useRef } from "react"; import SidebarLinks from "@/components/common/layouts/LayoutsNavigation/Sidebar/SidebarLinks"; import SidebarBrand from "@/components/common/layouts/LayoutsNavigation/Sidebar/SidebarBrand"; +import { useAutenticacion } from '@/components/common/utility/Autenticador'; export default function MobileMenu({ menuRef, linksItems }) { + const {cerrarSesion} = useAutenticacion(); const [viewSubMenuHelp, setViewSubMenuHelp] = useState(false); const firstContainerRef = useRef(null); @@ -59,7 +61,9 @@ export default function MobileMenu({ menuRef, linksItems }) {
{/* Enlace de cerrar sesión */} - + logout {/* Icono del ítem */} diff --git a/cosiap_frontend/src/components/common/layouts/LayoutsNavigation/Sidebar/Notifications.jsx b/cosiap_frontend/src/components/common/layouts/LayoutsNavigation/Sidebar/Notifications.jsx index a800e0012f22c41237febe4d4aaf0a16eb7a8db0..bbf4ba4c1d26aa2492e4e7d5e4328b6f42137d88 100644 --- a/cosiap_frontend/src/components/common/layouts/LayoutsNavigation/Sidebar/Notifications.jsx +++ b/cosiap_frontend/src/components/common/layouts/LayoutsNavigation/Sidebar/Notifications.jsx @@ -4,7 +4,7 @@ export default function Notifications( {notificationsRef} ){ return ( <> -
+
diff --git a/cosiap_frontend/src/components/common/layouts/LayoutsNavigation/Sidebar/Sidebar.jsx b/cosiap_frontend/src/components/common/layouts/LayoutsNavigation/Sidebar/Sidebar.jsx index e4fb6de23c995df62f9cb7ee636b898fa664828e..13056d9a5cca3b38158caa0209c8b85d44448aa1 100644 --- a/cosiap_frontend/src/components/common/layouts/LayoutsNavigation/Sidebar/Sidebar.jsx +++ b/cosiap_frontend/src/components/common/layouts/LayoutsNavigation/Sidebar/Sidebar.jsx @@ -6,8 +6,10 @@ import SidebarLinks from "@/components/common/layouts/LayoutsNavigation/Sidebar/ import SidebarBrand from "@/components/common/layouts/LayoutsNavigation/Sidebar/SidebarBrand"; import Notifications from "@/components/common/layouts/LayoutsNavigation/Sidebar/Notifications"; import MobileMenu from "@/components/common/layouts/LayoutsNavigation/Sidebar/MobileMenu"; +import { useAutenticacion } from '@/components/common/utility/Autenticador'; export default function Sidebar( {linksItems, viewMenu, setViewMenu} ){ + const {cerrarSesion} = useAutenticacion(); const navigate = useNavigate(); // Estado para controlar la visibilidad de las notificaciones const [viewNotifications, setViewNotifications] = useState(false); @@ -79,7 +81,7 @@ export default function Sidebar( {linksItems, viewMenu, setViewMenu} ){ setViewMenu={setViewMenu} />
-
+
{/* Enlace de cerrar sesión */} - + logout {/* Icono del ítem */} diff --git a/cosiap_frontend/src/components/common/utility/Autenticador.jsx b/cosiap_frontend/src/components/common/utility/Autenticador.jsx index dcb86540329a52f69d462911d5749c1202a60ac6..0a9d8e14e5b58011bce5f658d65f367db16fdc6b 100644 --- a/cosiap_frontend/src/components/common/utility/Autenticador.jsx +++ b/cosiap_frontend/src/components/common/utility/Autenticador.jsx @@ -21,35 +21,35 @@ export const useAutenticacion = () => { return contextoAut; }; -export const Autenticador = ({ children }) => { +export const Autenticador = ({ setViewPageLoader, children }) => { const [token, setToken] = useState(); + const [isAdmin, setIsAdmin] = useState(false); useEffect(() => { const buscarUsuario = async () => { - try { + try { const response = await api.usuarios.token.refresh(); setToken(response.data.access); + // IMPORTANTE: Primero se deben configurar los interceptores antes que determinar si es admin + configurarInterceptors(response.data.access); + const responseAd = await api.usuarios.admin.is_admin(); + setIsAdmin(responseAd.data.user_is_admin); + console.log(isAdmin) } catch { setToken(null); + setIsAdmin(false); } } buscarUsuario(); }, []); - useLayoutEffect(() => { + const configurarInterceptors = (token) => { const interceptadorAccess = api.axios.interceptors.request.use((config) => { config.headers.Authorization = !config.__retry && token ? `Bearer ${token}` : config.headers.Authorization; return config; }); - return () => { - api.axios.interceptors.request.eject(interceptadorAccess); - }; - - }, [token]); - - useLayoutEffect(() => { const interceptadorRefresh = api.axios.interceptors.response.use( (response) => response, async (error) => { const requestOriginal = error.config; @@ -60,10 +60,13 @@ export const Autenticador = ({ children }) => { setToken(response.data.access); requestOriginal.headers.Authorization = `Bearer ${response.data.access}`; requestOriginal.__retry = true; + const responseAd = await api.usuarios.admin.is_admin(); + setIsAdmin(responseAd.data.user_is_admin); return api(requestOriginal); } catch { setToken(null); + setIsAdmin(false); } } @@ -71,12 +74,34 @@ export const Autenticador = ({ children }) => { }, ); + // Eject interceptors on component unmount return () => { + api.axios.interceptors.request.eject(interceptadorAccess); api.axios.interceptors.response.eject(interceptadorRefresh); }; - }); + }; + + useLayoutEffect(() => { + if (token) { + const ejectInterceptors = configurarInterceptors(token); + return () => ejectInterceptors(); + } + }, [token]); + + const cerrarSesion = async () => { + setViewPageLoader(true); + try { + const response = await api.usuarios.token.logout(); + console.log('Logout succesful ', response); + setToken(null); + } catch (error) { + console.error('Logout unsuccesful ', error); + } + setViewPageLoader(false); + }; + return ( - + {children} ); @@ -84,4 +109,4 @@ export const Autenticador = ({ children }) => { Autenticador.propTypes = { children: PropTypes.node, -}; \ No newline at end of file +}; diff --git a/cosiap_frontend/src/components/common/utility/LoginRequiredRoute.jsx b/cosiap_frontend/src/components/common/utility/LoginRequiredRoute.jsx new file mode 100644 index 0000000000000000000000000000000000000000..33607d6acda8f2e8f130cf4e9fd66f7a4924cec9 --- /dev/null +++ b/cosiap_frontend/src/components/common/utility/LoginRequiredRoute.jsx @@ -0,0 +1,55 @@ +import { useAutenticacion } from "@/components/common/utility/Autenticador"; +import { useState, useEffect } from "react"; +import { Navigate, Outlet } from "react-router-dom"; +import api from "@/api"; + +export const LoginRequiredRoute = () => { + const { token, isAdmin } = useAutenticacion(); + console.log(isAdmin) + if (token === null) { + return ; + } else if (token === undefined) { + return null; + }else { + return ; + } +}; + +export const IsAdminRequiredRoute = ({ setViewPageLoader }) => { + const [isAdmin, setIsAdmin] = useState(false); + const [hasCheckedAdmin, setHasCheckedAdmin] = useState(false); // Nuevo estado para verificar si ya se hizo la comprobación + + useEffect(() => { + const verificarAdmin = async () => { + setViewPageLoader(true); // Muestra el loader + try { + const response = await api.usuarios.admin.is_admin(); + setIsAdmin(response.data.user_is_admin); + } catch (error) { + console.error('Determinación de permisos admin ha fallado', error); + setIsAdmin(false); + } finally { + setHasCheckedAdmin(true); // Indica que la verificación ha terminado + setViewPageLoader(false); // Oculta el loader después de recibir la respuesta + } + }; + + verificarAdmin(); + }, [setViewPageLoader]); + + if (!hasCheckedAdmin) { + // Mientras no se haya verificado si es admin o no, retorna null + return null; + } + + return isAdmin ? : ; +}; + +export const IsLogged = () => { + const { token } = useAutenticacion(); + if (token === null) { + return + }else{ + return ; + } +}; diff --git a/cosiap_frontend/src/components/common/utility/LoginRequiredRoutes.jsx b/cosiap_frontend/src/components/common/utility/LoginRequiredRoutes.jsx deleted file mode 100644 index 784e8ac1728fe7edff70184fe8ca04d0f1e28bb1..0000000000000000000000000000000000000000 --- a/cosiap_frontend/src/components/common/utility/LoginRequiredRoutes.jsx +++ /dev/null @@ -1,16 +0,0 @@ -import { useAutenticacion } from "@/components/common/utility/Autenticador"; -import { Navigate, Outlet } from "react-router-dom"; - -export const LoginRequiredRoutes = () => { - const { token } = useAutenticacion(); - - if (token === null) { - return ; - } else if (token === undefined) { - return null; // O un spinner de carga o marcador de posición - } else { - return ; - } -}; - -export default LoginRequiredRoutes; \ No newline at end of file diff --git a/cosiap_frontend/src/components/users/Login/LoginForm.jsx b/cosiap_frontend/src/components/users/Login/LoginForm.jsx index b5b820cb2408f19fe6993b916df45889f53ca4c7..57d0a42c7a6039357361faa68558fe99816db920 100644 --- a/cosiap_frontend/src/components/users/Login/LoginForm.jsx +++ b/cosiap_frontend/src/components/users/Login/LoginForm.jsx @@ -16,7 +16,7 @@ import {useAutenticacion} from "@/components/common/utility/Autenticador" export function LoginForm( {setViewPageLoader} ) { const [loginError, setLoginError] = useState('') const navigate = useNavigate(); - const { setToken } = useAutenticacion(); + const { setToken, setIsAdmin, isAdmin } = useAutenticacion(); const { register, handleSubmit, @@ -30,7 +30,10 @@ export function LoginForm( {setViewPageLoader} ) { try { const response = await api.usuarios.token.login(data); console.log("Login successful:", response.data); - setToken(response.data.access) + setToken(response.data.access) + //Establecemos si es admin o no + const responseAd = await api.usuarios.admin.is_admin(); + setIsAdmin(responseAd.data.user_is_admin); navigate("/inicio"); } catch (error) { console.error("Login failed:", error); diff --git a/cosiap_frontend/src/components/users/NoAutorizado.jsx b/cosiap_frontend/src/components/users/NoAutorizado.jsx new file mode 100644 index 0000000000000000000000000000000000000000..cda20323a90e1411779959aeb638aec29040ce4b --- /dev/null +++ b/cosiap_frontend/src/components/users/NoAutorizado.jsx @@ -0,0 +1,29 @@ +import { useNavigate } from "react-router-dom"; +export default function NoAutorizado() { + const navigate = useNavigate(); + + return ( + + ); +} diff --git a/cosiap_frontend/src/components/users/PageNotFound.jsx b/cosiap_frontend/src/components/users/PageNotFound.jsx new file mode 100644 index 0000000000000000000000000000000000000000..3c0ce5dedf5a2b9f673dd50f023c9fff392f8a22 --- /dev/null +++ b/cosiap_frontend/src/components/users/PageNotFound.jsx @@ -0,0 +1,29 @@ +import { useNavigate } from "react-router-dom"; +export default function NoAutorizado() { + const navigate = useNavigate(); + + return ( +
+
+
+ Pagina no encontrada +
+
+ La pagina solicitada no existe +
+ +
+
+ ); +}