Saltearse al contenido

Componente Protected

Un componente que restringe el acceso a sus hijos (UI) según el estado de autenticación de la persona usuaria. Si la persona usuaria no está autenticada, redirige a la página establecida, de lo contrario renderiza sus hijos. Se utiliza dentro del archivo donde se definen las rutas, que puede ser el mismo donde se define el componente enrutador. Se recomienda que este archivo sea MichiRouter.tsx.

Toma una prop y un children. La prop es un objeto de configuración con las siguientes propiedades:

  • states (obligatorio): Record<string, any> objeto con { user: any, isLoading: boolean }. Por ejemplo el valor retornado por un hook de autenticación (Zustand, Redux, Context, etc.). Se puede pasar directamente el store si este retorna un objeto con esas dos propiedades.
    • user: any → el objeto o valor del usuario actual. Si es null o undefined, se considera que no hay usuario autenticado.
    • isLoading: boolean → indica si el proceso de autenticación/carga está en curso.
  • redirectionPath (obligatorio): string ruta destino cuando no hay usuario autenticado. Por defecto /.
  • loadingComponent (opcional): JSX.Element componente React a mostrar mientras isLoading es true.
  • defaultMessage (opcional): string. Mensaje por defecto a mostrar si no se provee loadingComponent. Si no se provee ningún mensaje, no se muestra nada.

El children es el contenido que se renderizará si la persona usuaria está autenticada.

  • children (obligatorio): ReactNode contenido protegido.
import React, { useEffect } from "react";
import { useNavigate } from "./Michi-router";
import { ProtectedProps } from "./types";
/**
* Un componente que restringe el acceso a sus hijos según el estado de autenticación del usuario.
*
* Si el usuario no está autenticado, redirige a la página de landing ("/").
* De lo contrario, renderiza sus hijos.
*
* @param {Object} props - Props del componente
* @param {React.ReactNode} props.children - Los nodos React a renderizar si el usuario está autenticado.
* @returns {JSX.Element|null} Los hijos si está autenticado, de lo contrario loader.
*/
export default function Protected({
children,
configObject,
}: ProtectedProps): JSX.Element | null {
const navigate = useNavigate();
/**
* Objeto de configuración para el manejo de rutas protegidas.
*
* @property {Object} states - Contiene el estado actual del usuario y el estado de carga.
* @property {any} states.user - El objeto o valor del usuario actual.
* @property {boolean} states.isLoading - Indica si el proceso de autenticación/carga está en curso.
* @property {string} redirectionPath - Ruta a la que se redirige si el usuario no está autenticado.
* @property {React.ReactNode} [loadingComponent] - Componente personalizado opcional para mostrar mientras carga.
* @property {string} [defaultMessage] - Mensaje por defecto a mostrar si no se provee loadingComponent. Si no se provee ningún mensaje, no se muestra nada.
*/
const config: {
states: { user: any; isLoading: boolean };
redirectionPath: string;
loadingComponent?: React.ReactNode;
defaultMessage?: string;
} = {
states: configObject?.states || null,
redirectionPath: configObject?.redirectionPath || "/",
loadingComponent: configObject?.loadingComponent || null,
defaultMessage: configObject?.defaultMessage || undefined,
};
if (!config.states) {
console.error(
"Componente Protected: El objeto de configuración es inválido. Este es el formato esperado:\n{\n states: { user: any; isLoading: boolean };\n redirectionPath: string;\n loadingComponent?: React.ReactNode;\n defaultMessage?: string;\n}"
);
return null;
}
// Leemos el estado directamente desde el store
const { user, isLoading } = config.states;
useEffect(() => {
// Redirigir solo cuando haya terminado de cargar y el usuario NO esté autenticado
if (!isLoading && !user) {
navigate(config.redirectionPath);
}
}, [isLoading, user, navigate, config.redirectionPath]);
// Mientras carga, mostrar loadingComponent si está definido, si no y defaultMessage true mostrar texto
if (isLoading) {
if (config.loadingComponent) return config.loadingComponent as JSX.Element;
return config.defaultMessage ? <>{config.defaultMessage}</> : null;
}
return user ? children : null;
}
// ...otros imports
import {
Protected,
RouterProvider as MichiProvider,
} from "@arielgonzaguer/michi-router";
import useAuthStore from "../store/useAuthStore"; // la store que maneja autenticación
import Login from "../paginas/Login.jsx";
import Home from "../paginas/Home.jsx";
import Notas from "../paginas/Notas.jsx";
// ...otros componentes
export default function MichiRouter() {
const configObject = {
states: useAuthStore(),
redirectionPath: "/",
loadingComponent: (
<div className="w-full h-screen flex items-center justify-center">
Cargando...
</div>
),
defaultMessage: "Cargando autenticación...",
};
const routes = [
{ path: "/", component: <Home /> },
{
path: "/login",
component: <Login />,
},
{
path: "/notas",
component: (
<Protected configObject={configObject}>
<Notas />
</Protected>
),
},
// ...otras rutas
];
return (
<RouterProvider routes={rutas} layout={BaseLayout}>
<NotFoud404 />
</RouterProvider>
);
}

El componente ayuda a proteger rutas en el frontend, mostrando un loader o redirigiendo si el usuario no está autenticado. Sin embargo, NO reemplaza las reglas de seguridad en el backend, siempre se deben usar reglas de Firestore -si se usa- o validaciones en su API.

  1. Tener una store de autenticación. La store debe proveer:
  • user: null | undefined | object

    • undefined → aún no sabemos si hay usuario (fase de carga inicial).
    • null → no hay usuario autenticado.
    • object → usuario autenticado.
  • isLoading: boolean

    • true → la app está verificando la sesión.
    • false → ya se verificó (haya o no usuario).
  1. Haber implementado un listener de autenticación (por ejemplo, onAuthStateChanged en Firebase) que actualice la store antes de montar el router.
  • Siempre inicialice isLoading en true hasta que se termine de verificar la sesión. Así se evitan redirecciones prematuras y el efecto “parpadeo”.
  • Use un loadingComponent para personalizar la experiencia.

Ejemplo con spinner:

<Protected
configObject={{
states: { user, isLoading },
redirectionPath: "/",
loadingComponent: <Spinner />,
}}
>
<PanelPrivado />
</Protected>
  • Evite hacer side-effects dentro de Protected. El componente solo debería redirigir o mostrar loader, no cargar datos ni mutar estado global.
rules_version = '2';
service cloud.firestore { match /databases/{database}/documents{
// Permitir acceso al documento solo a usuarios autenticados
match /emergenciaData/casa {
allow read, write: if request.auth != null;
}
// Denegar todo lo demás
match /{document=**} {
allow read, write: if false;
}
}
}