Saltearse al contenido

Integración con Firebase Auth

Esta guía le mostrará cómo integrar MichiRouter con Firebase Authentication para crear un sistema de rutas protegidas robusto y fácil de usar.

Ventana de terminal
npm install @arielgonzaguer/michi-router firebase

Crea un archivo firebase.js en tu proyecto:

import { initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';
import { getFirestore } from 'firebase/firestore';
const firebaseConfig = {
apiKey: "tu-api-key",
authDomain: "tu-proyecto.firebaseapp.com",
projectId: "tu-proyecto-id",
storageBucket: "tu-proyecto.appspot.com",
messagingSenderId: "123456789",
appId: "tu-app-id"
};
const app = initializeApp(firebaseConfig);
export const auth = getAuth(app);
export const db = getFirestore(app);

Crear un store para manejar el estado de autenticación:

store/useAuthStore.ts
import { create } from 'zustand';
import {
signInWithEmailAndPassword,
createUserWithEmailAndPassword,
signOut,
onAuthStateChanged,
User
} from 'firebase/auth';
import { auth } from '../firebase';
interface AuthState {
user: User | null;
isLoading: boolean;
error: string | null;
signIn: (email: string, password: string) => Promise<void>;
signUp: (email: string, password: string) => Promise<void>;
logout: () => Promise<void>;
initializeAuth: () => void;
}
const useAuthStore = create<AuthState>((set, get) => ({
user: null,
isLoading: true, // Importante: iniciar en true
error: null,
signIn: async (email: string, password: string) => {
try {
set({ isLoading: true, error: null });
await signInWithEmailAndPassword(auth, email, password);
} catch (error: any) {
set({ error: error.message, isLoading: false });
}
},
signUp: async (email: string, password: string) => {
try {
set({ isLoading: true, error: null });
await createUserWithEmailAndPassword(auth, email, password);
} catch (error: any) {
set({ error: error.message, isLoading: false });
}
},
logout: async () => {
try {
await signOut(auth);
} catch (error: any) {
set({ error: error.message });
}
},
initializeAuth: () => {
onAuthStateChanged(auth, (user) => {
set({
user,
isLoading: false,
error: null
});
});
}
}));
export default useAuthStore;
components/Login.tsx
import React, { useState } from 'react';
import { useNavigate } from '@arielgonzaguer/michi-router';
import useAuthStore from '../store/useAuthStore';
export default function Login() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const { signIn, isLoading, error } = useAuthStore();
const navigate = useNavigate();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
await signIn(email, password);
navigate('/dashboard'); // Redirigir tras login exitoso
} catch (error) {
console.error('Error al iniciar sesión:', error);
}
};
return (
<div className="min-h-screen flex items-center justify-center">
<form onSubmit={handleSubmit} className="max-w-md mx-auto space-y-4">
<h2 className="text-2xl font-bold text-center">Iniciar Sesión</h2>
{error && (
<div className="p-3 text-red-600 bg-red-100 rounded">
{error}
</div>
)}
<input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full p-3 border rounded"
required
/>
<input
type="password"
placeholder="Contraseña"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full p-3 border rounded"
required
/>
<button
type="submit"
disabled={isLoading}
className="w-full p-3 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
>
{isLoading ? 'Iniciando sesión...' : 'Iniciar Sesión'}
</button>
</form>
</div>
);
}
MichiRouter.tsx
import React, { useEffect } from 'react';
import {
RouterProvider,
Protected
} from '@arielgonzaguer/michi-router';
import useAuthStore from './store/useAuthStore';
// Componentes
import Home from './components/Home';
import Login from './components/Login';
import Dashboard from './components/Dashboard';
import Profile from './components/Profile';
import NotFound from './components/NotFound';
import LoadingSpinner from './components/LoadingSpinner';
export default function MichiRouter() {
const authStore = useAuthStore();
// Inicializar listener de autenticación
useEffect(() => {
authStore.initializeAuth();
}, []);
// Configuración para rutas protegidas
const protectedConfig = {
states: {
user: authStore.user,
isLoading: authStore.isLoading
},
redirectionPath: "/login",
loadingComponent: <LoadingSpinner />,
defaultMessage: "Verificando autenticación..."
};
const routes = [
{
path: "/",
component: <Home />
},
{
path: "/login",
component: <Login />
},
{
path: "/dashboard",
component: (
<Protected configObject={protectedConfig}>
<Dashboard />
</Protected>
)
},
{
path: "/profile",
component: (
<Protected configObject={protectedConfig}>
<Profile />
</Protected>
)
}
];
return (
<RouterProvider routes={routes}>
<NotFound />
</RouterProvider>
);
}
components/Dashboard.tsx
import React from 'react';
import { useNavigate } from '@arielgonzaguer/michi-router';
import useAuthStore from '../store/useAuthStore';
export default function Dashboard() {
const { user, logout } = useAuthStore();
const navigate = useNavigate();
const handleLogout = async () => {
await logout();
navigate('/');
};
return (
<div className="p-6">
<div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold">Dashboard</h1>
<button
onClick={handleLogout}
className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
>
Cerrar Sesión
</button>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<h2 className="text-xl font-semibold mb-4">Bienvenido</h2>
<p>Email: {user?.email}</p>
<p>UID: {user?.uid}</p>
</div>
</div>
);
}

Para completar la seguridad, configure las reglas de Firestore:

rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Permitir acceso solo a usuarios autenticados
match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
// Datos públicos (si los hay)
match /public/{document} {
allow read: if true;
allow write: if request.auth != null;
}
// Denegar todo lo demás
match /{document=**} {
allow read, write: if false;
}
}
}
  • Siempre inicializa isLoading en true hasta verificar la sesión
  • Usa el listener onAuthStateChanged para manejar cambios de estado
// Redirigir usuarios autenticados lejos del login
useEffect(() => {
if (!isLoading && user && location.pathname === '/login') {
navigate('/dashboard');
}
}, [isLoading, user, location.pathname]);
// Guardar la ruta donde el usuario quería ir
const protectedConfig = {
states: { user: authStore.user, isLoading: authStore.isLoading },
redirectionPath: `/login?redirect=${encodeURIComponent(location.pathname)}`,
// ...
};
// En el componente Login
const urlParams = new URLSearchParams(location.search);
const redirectTo = urlParams.get('redirect') || '/dashboard';
// Tras login exitoso
navigate(redirectTo);

Este setup le proporciona:

  • ✅ Autenticación completa con Firebase
  • ✅ Rutas protegidas con MichiRouter
  • ✅ Estado de carga apropiado
  • ✅ Redirecciones automáticas
  • ✅ Manejo de errores
  • ✅ Experiencia de usuario fluida

El componente Protected en este ejemplo se encarga automáticamente de:

  • Mostrar un spinner mientras se verifica la autenticación
  • Redirigir a login si no hay usuario
  • Renderizar el contenido protegido si el usuario está autenticado

Esta integración aprovecha al máximo la simplicidad de MichiRouter manteniendo toda la funcionalidad robusta de Firebase Auth.