Frontend Routing

This document describes the routing configuration, route guards, and navigation patterns in the Frontend service.

Overview

The application uses React Router DOM v7 with a role-based routing system. Routes are protected by guard components that check authentication status and user roles.

Route Structure

graph TB
    subgraph "Public Routes"
        Login["/login"]
        Register["/register"]
    end
    
    subgraph "Protected Routes"
        subgraph "Student Routes"
            Chat["/chat"]
        end
        
        subgraph "Professor Routes"
            Dashboard["/dashboard"]
        end
        
        subgraph "Admin Routes"
            Admin["/admin"]
            AdminUsers["/admin/users"]
            AdminSubjects["/admin/subjects"]
        end
        
        subgraph "Common Routes"
            Settings["/settings"]
        end
    end
    
    Root["/"] --> |redirect| DefaultRedirect
    DefaultRedirect --> |student| Chat
    DefaultRedirect --> |professor| Dashboard
    DefaultRedirect --> |admin| Admin

Route Configuration

// src/App.tsx

function App() {
  return (
    <Router>
      <Routes>
        {/* Public routes */}
        <Route element={<PublicRoute />}>
          <Route path="/login" element={<LoginPage />} />
          <Route path="/register" element={<RegisterPage />} />
        </Route>

        {/* Protected routes with AppLayout */}
        <Route element={<RequireAuth />}>
          <Route element={<AppLayout />}>
            {/* Student-only routes */}
            <Route element={<RequireStudent />}>
              <Route path="/chat" element={<ChatPage />} />
            </Route>

            {/* Professor/Admin routes */}
            <Route element={<RequireProfessor />}>
              <Route path="/dashboard" element={<DashboardPage />} />
            </Route>

            {/* Admin-only routes */}
            <Route element={<RequireAdmin />}>
              <Route path="/admin" element={<AdminDashboard />} />
            </Route>

            {/* Common protected routes */}
            <Route path="/settings" element={<SettingsPage />} />

            {/* Default redirect based on role */}
            <Route path="/" element={<DefaultRedirect />} />
          </Route>
        </Route>

        {/* Catch-all redirect */}
        <Route path="*" element={<Navigate to="/" replace />} />
      </Routes>
    </Router>
  );
}

Route Table

Path Component Access Description
/login LoginPage Public only User login form
/register RegisterPage Public only User registration
/chat ChatPage Student AI chat interface
/dashboard DashboardPage Professor, Admin Course management
/admin AdminDashboard Admin Platform administration
/settings SettingsPage All authenticated User preferences
/ DefaultRedirect All authenticated Role-based redirect

Route Guards

Guard Hierarchy

graph TB
    Request[Route Request]
    
    Request --> PublicRoute
    Request --> RequireAuth
    
    PublicRoute --> |authenticated?| Redirect[Redirect to app]
    PublicRoute --> |not authenticated| PublicPage[Public Page]
    
    RequireAuth --> |not authenticated?| Login[Redirect to /login]
    RequireAuth --> |authenticated| RoleGuards
    
    RoleGuards --> RequireStudent
    RoleGuards --> RequireProfessor
    RoleGuards --> RequireAdmin
    
    RequireStudent --> |student| StudentPage[Student Page]
    RequireStudent --> |other| RoleRedirect[Role Redirect]
    
    RequireProfessor --> |professor/admin| ProfPage[Professor Page]
    RequireProfessor --> |student| ChatRedirect["/chat"]
    
    RequireAdmin --> |admin| AdminPage[Admin Page]
    RequireAdmin --> |other| RoleRedirect2[Role Redirect]

PublicRoute

Prevents authenticated users from accessing login/register pages.

// src/components/layout/PublicRoute.tsx
import { Navigate, Outlet } from "react-router-dom";
import { useAuth } from "@/context/AuthContext";

export default function PublicRoute() {
  const { isAuthenticated, user } = useAuth();

  if (isAuthenticated) {
    // Redirect authenticated users to their default page
    switch (user?.role) {
      case "student":
        return <Navigate to="/chat" replace />;
      case "professor":
        return <Navigate to="/dashboard" replace />;
      case "admin":
        return <Navigate to="/admin" replace />;
      default:
        return <Navigate to="/" replace />;
    }
  }

  return <Outlet />;
}

RequireAuth

Base authentication guard that redirects unauthenticated users.

// src/components/layout/RequireAuth.tsx
import { Navigate, Outlet, useLocation } from "react-router-dom";
import { useAuth } from "@/context/AuthContext";

export default function RequireAuth() {
  const { isAuthenticated } = useAuth();
  const location = useLocation();

  if (!isAuthenticated) {
    // Save intended destination for redirect after login
    return <Navigate to="/login" state= replace />;
  }

  return <Outlet />;
}

RequireStudent

Restricts access to student role only.

// src/components/layout/RequireStudent.tsx
import { Navigate, Outlet } from "react-router-dom";
import { useAuth } from "@/context/AuthContext";

export default function RequireStudent() {
  const { user } = useAuth();

  if (user?.role !== "student") {
    // Redirect non-students to their appropriate page
    return user?.role === "admin" 
      ? <Navigate to="/admin" replace />
      : <Navigate to="/dashboard" replace />;
  }

  return <Outlet />;
}

RequireProfessor

Allows professors and admins (admins inherit professor permissions).

// src/components/layout/RequireProfessor.tsx
import { Navigate, Outlet } from "react-router-dom";
import { useAuth } from "@/context/AuthContext";

export default function RequireProfessor() {
  const { user } = useAuth();

  if (user?.role !== "professor" && user?.role !== "admin") {
    return <Navigate to="/chat" replace />;
  }

  return <Outlet />;
}

RequireAdmin

Restricts access to admin role only.

// src/components/layout/RequireAdmin.tsx
import { Navigate, Outlet } from "react-router-dom";
import { useAuth } from "@/context/AuthContext";

export default function RequireAdmin() {
  const { user } = useAuth();

  if (user?.role !== "admin") {
    return user?.role === "professor" 
      ? <Navigate to="/dashboard" replace />
      : <Navigate to="/chat" replace />;
  }

  return <Outlet />;
}

DefaultRedirect

Redirects to the appropriate page based on user role.

// src/components/layout/DefaultRedirect.tsx
import { Navigate } from "react-router-dom";
import { useAuth } from "@/context/AuthContext";

export default function DefaultRedirect() {
  const { user, isAuthenticated } = useAuth();

  if (!isAuthenticated) {
    return <Navigate to="/login" replace />;
  }

  switch (user?.role) {
    case "student":
      return <Navigate to="/chat" replace />;
    case "professor":
      return <Navigate to="/dashboard" replace />;
    case "admin":
      return <Navigate to="/admin" replace />;
    default:
      return <Navigate to="/login" replace />;
  }
}

The AppLayout component renders navigation based on user role:

// Navigation items configuration
const navItems = [
  { 
    to: "/chat", 
    label: "Chat", 
    icon: MessageSquare, 
    roles: ["student"] 
  },
  {
    to: "/dashboard",
    label: "Mis Clases",
    icon: GraduationCap,
    roles: ["professor", "admin"],
    requiresSubjects: true, // Only show if user has subjects
  },
  { 
    to: "/admin", 
    label: "Admin", 
    icon: LayoutDashboard, 
    roles: ["admin"] 
  },
  { 
    to: "/settings", 
    label: "Configuración", 
    icon: Settings 
    // No roles = visible to all
  },
];

// Filter logic
const filteredNavItems = navItems.filter((item) => {
  const roleAllowed = !item.roles || item.roles.includes(user?.role);
  const subjectsAllowed = !item.requiresSubjects || user?.subjects?.length > 0;
  return roleAllowed && subjectsAllowed;
});
Role Visible Nav Items
Student Chat, Settings
Professor Dashboard*, Settings
Admin Dashboard*, Admin, Settings

*Only if user has subjects assigned

<NavLink
  to={item.to}
  className={({ isActive }) =>
    cn(
      "flex items-center gap-3 px-3 py-2 rounded-md",
      isActive
        ? "bg-primary text-primary-foreground"
        : "text-muted-foreground hover:bg-muted"
    )
  }
>
  <item.icon className="h-4 w-4" />
  {item.label}
</NavLink>

Programmatic Navigation

Using useNavigate

import { useNavigate } from "react-router-dom";

function LoginPage() {
  const navigate = useNavigate();
  const { login } = useAuth();

  const onSubmit = async (data) => {
    const { token, user } = await authenticate(data);
    login(token, user);
    
    // Navigate after login
    navigate("/chat");
  };
}

Post-Login Redirect

// In RequireAuth, save the intended destination
<Navigate to="/login" state= replace />

// In LoginPage, redirect to saved location
const location = useLocation();
const from = location.state?.from?.pathname || "/";

const onLoginSuccess = () => {
  navigate(from, { replace: true });
};

Logout Navigation

function AppLayout() {
  const { logout } = useAuth();
  const navigate = useNavigate();

  const handleLogout = () => {
    logout();
    navigate("/login");
  };
}

Route Access Matrix

graph TB
    subgraph "Not Authenticated"
        NA1["/login ✓"]
        NA2["/register ✓"]
        NA3["/chat ✗ → /login"]
        NA4["/dashboard ✗ → /login"]
        NA5["/admin ✗ → /login"]
    end
    
    subgraph "Student"
        S1["/login → /chat"]
        S2["/chat ✓"]
        S3["/dashboard → /chat"]
        S4["/admin → /chat"]
        S5["/settings ✓"]
    end
    
    subgraph "Professor"
        P1["/login → /dashboard"]
        P2["/chat → /dashboard"]
        P3["/dashboard ✓"]
        P4["/admin → /dashboard"]
        P5["/settings ✓"]
    end
    
    subgraph "Admin"
        A1["/login → /admin"]
        A2["/chat → /admin"]
        A3["/dashboard ✓"]
        A4["/admin ✓"]
        A5["/settings ✓"]
    end
Route Not Auth Student Professor Admin
/login → /chat → /dashboard → /admin
/register → /chat → /dashboard → /admin
/chat → /login → /dashboard → /admin
/dashboard → /login → /chat
/admin → /login → /chat → /dashboard
/settings → /login
/ → /login → /chat → /dashboard → /admin

Deep Linking

The application supports deep linking for:

/chat?session=<session-id>

Admin Tabs

/admin?tab=users
/admin?tab=subjects

Example Implementation

function AdminDashboard() {
  const [searchParams, setSearchParams] = useSearchParams();
  const activeTab = searchParams.get("tab") || "overview";

  const handleTabChange = (tab: string) => {
    setSearchParams({ tab });
  };

  return (
    <Tabs value={activeTab} onValueChange={handleTabChange}>
      <TabsList>
        <TabsTrigger value="overview">Overview</TabsTrigger>
        <TabsTrigger value="users">Users</TabsTrigger>
        <TabsTrigger value="subjects">Subjects</TabsTrigger>
      </TabsList>
      {/* ... */}
    </Tabs>
  );
}

Error Handling

404 Handling

// Catch-all route redirects to home
<Route path="*" element={<Navigate to="/" replace />} />

Auth State Changes

When auth state changes (e.g., token expires), the user is redirected:

// In api.ts interceptor
api.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      // Token expired or invalid
      localStorage.removeItem("token");
      localStorage.removeItem("user");
      window.location.href = "/login";
    }
    return Promise.reject(error);
  }
);

Mobile Navigation

On mobile devices, the sidebar is collapsible:

function AppLayout() {
  const [sidebarOpen, setSidebarOpen] = useState(false);

  return (
    <div className="flex h-screen">
      {/* Mobile overlay */}
      {sidebarOpen && (
        <div
          className="fixed inset-0 bg-black/50 z-40 md:hidden"
          onClick={() => setSidebarOpen(false)}
        />
      )}

      {/* Sidebar */}
      <aside
        className={cn(
          "fixed md:static inset-y-0 left-0 z-50 w-64",
          "transform transition-transform duration-200",
          sidebarOpen ? "translate-x-0" : "-translate-x-full md:translate-x-0"
        )}
      >
        {/* Sidebar content */}
      </aside>

      {/* Main content */}
      <main className="flex-1">
        {/* Mobile menu button */}
        <button
          className="md:hidden"
          onClick={() => setSidebarOpen(true)}
        >
          <Menu />
        </button>
        <Outlet />
      </main>
    </div>
  );
}