Files
Hotel-Booking/docs/ROUTE_PROTECTION.md
Iliyan Angelov 93d4c1df80 update
2025-11-16 15:12:43 +02:00

12 KiB

Route Protection Documentation

Function 8: Authorization & Route Protection

The system uses 2 components to protect routes:

  • ProtectedRoute: Requires user to be logged in
  • AdminRoute: Requires user to be Admin

1. ProtectedRoute

Purpose

Protects routes requiring authentication (login).

How It Works

// File: client/src/components/auth/ProtectedRoute.tsx

const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ 
  children 
}) => {
  const location = useLocation();
  const { isAuthenticated, isLoading } = useAuthStore();

  // 1. If loading → display spinner
  if (isLoading) {
    return <LoadingScreen />;
  }

  // 2. If not logged in → redirect /login
  if (!isAuthenticated) {
    return (
      <Navigate 
        to="/login" 
        state={{ from: location }}  // Save location to return later
        replace 
      />
    );
  }

  // 3. Logged in → allow access
  return <>{children}</>;
};

Usage in App.tsx

import { ProtectedRoute } from './components/auth';

// Route requiring login
<Route 
  path="/dashboard" 
  element={
    <ProtectedRoute>
      <DashboardPage />
    </ProtectedRoute>
  } 
/>

<Route 
  path="/bookings" 
  element={
    <ProtectedRoute>
      <BookingListPage />
    </ProtectedRoute>
  } 
/>

<Route 
  path="/profile" 
  element={
    <ProtectedRoute>
      <ProfilePage />
    </ProtectedRoute>
  } 
/>

Flow

  1. User not logged in accesses /dashboard
  2. ProtectedRoute checks isAuthenticated === false
  3. Redirect to /login and save state={{ from: '/dashboard' }}
  4. After successful login, redirect to /dashboard

2. AdminRoute

Purpose

Protects routes for Admin only (role-based access).

How It Works

// File: client/src/components/auth/AdminRoute.tsx

const AdminRoute: React.FC<AdminRouteProps> = ({ 
  children 
}) => {
  const location = useLocation();
  const { isAuthenticated, userInfo, isLoading } = useAuthStore();

  // 1. If loading → display spinner
  if (isLoading) {
    return <LoadingScreen />;
  }

  // 2. If not logged in → redirect /login
  if (!isAuthenticated) {
    return (
      <Navigate 
        to="/login" 
        state={{ from: location }} 
        replace 
      />
    );
  }

  // 3. If not admin → redirect /
  const isAdmin = userInfo?.role === 'admin';
  if (!isAdmin) {
    return <Navigate to="/" replace />;
  }

  // 4. Is admin → allow access
  return <>{children}</>;
};

Usage in App.tsx

import { AdminRoute } from './components/auth';

// Route for Admin only
<Route 
  path="/admin" 
  element={
    <AdminRoute>
      <AdminLayout />
    </AdminRoute>
  }
>
  <Route path="dashboard" element={<AdminDashboard />} />
  <Route path="users" element={<UserManagement />} />
  <Route path="rooms" element={<RoomManagement />} />
  <Route path="bookings" element={<BookingManagement />} />
  <Route path="settings" element={<Settings />} />
</Route>

Flow

Case 1: User not logged in

  1. Access /admin
  2. AdminRoute checks isAuthenticated === false
  3. Redirect to /login with state={{ from: '/admin' }}
  4. After successful login → return to /admin
  5. AdminRoute checks role again

Case 2: User logged in but not Admin

  1. Customer (role='customer') accesses /admin
  2. AdminRoute checks isAuthenticated === true
  3. AdminRoute checks userInfo.role === 'customer' (not 'admin')
  4. Redirect to / (homepage)

Case 3: User is Admin

  1. Admin (role='admin') accesses /admin
  2. AdminRoute checks isAuthenticated === true
  3. AdminRoute checks userInfo.role === 'admin'
  4. Allow access

3. Route Structure in App.tsx

function App() {
  return (
    <BrowserRouter>
      <Routes>
        {/* Public Routes - No login required */}
        <Route path="/" element={<LayoutMain />}>
          <Route index element={<HomePage />} />
          <Route path="rooms" element={<RoomListPage />} />
          <Route path="about" element={<AboutPage />} />
        </Route>

        {/* Auth Routes - No layout */}
        <Route path="/login" element={<LoginPage />} />
        <Route path="/register" element={<RegisterPage />} />
        <Route path="/forgot-password" element={<ForgotPasswordPage />} />
        <Route path="/reset-password/:token" element={<ResetPasswordPage />} />

        {/* Protected Routes - Login required */}
        <Route path="/" element={<LayoutMain />}>
          <Route 
            path="dashboard" 
            element={
              <ProtectedRoute>
                <DashboardPage />
              </ProtectedRoute>
            } 
          />
          <Route 
            path="bookings" 
            element={
              <ProtectedRoute>
                <BookingListPage />
              </ProtectedRoute>
            } 
          />
          <Route 
            path="profile" 
            element={
              <ProtectedRoute>
                <ProfilePage />
              </ProtectedRoute>
            } 
          />
        </Route>

        {/* Admin Routes - Admin only */}
        <Route 
          path="/admin" 
          element={
            <AdminRoute>
              <AdminLayout />
            </AdminRoute>
          }
        >
          <Route index element={<Navigate to="dashboard" replace />} />
          <Route path="dashboard" element={<AdminDashboard />} />
          <Route path="users" element={<UserManagement />} />
          <Route path="rooms" element={<RoomManagement />} />
          <Route path="bookings" element={<BookingManagement />} />
          <Route path="payments" element={<PaymentManagement />} />
          <Route path="services" element={<ServiceManagement />} />
          <Route path="promotions" element={<PromotionManagement />} />
          <Route path="banners" element={<BannerManagement />} />
          <Route path="reports" element={<Reports />} />
          <Route path="settings" element={<Settings />} />
        </Route>

        {/* 404 Route */}
        <Route path="*" element={<NotFoundPage />} />
      </Routes>
    </BrowserRouter>
  );
}

4. Integration with Zustand Store

useAuthStore State

// File: client/src/store/useAuthStore.ts

const useAuthStore = create<AuthStore>((set) => ({
  // State
  token: localStorage.getItem('token') || null,
  refreshToken: localStorage.getItem('refreshToken') || null,
  userInfo: JSON.parse(localStorage.getItem('userInfo') || 'null'),
  isAuthenticated: !!localStorage.getItem('token'),
  isLoading: false,
  error: null,

  // Actions
  login: async (credentials) => { ... },
  logout: () => {
    localStorage.removeItem('token');
    localStorage.removeItem('refreshToken');
    localStorage.removeItem('userInfo');
    set({
      token: null,
      refreshToken: null,
      userInfo: null,
      isAuthenticated: false,
      error: null
    });
  },
  
  // ... other actions
}));

User Roles

  • admin: Administrator (full access)
  • staff: Staff (limited access)
  • customer: Customer (customer features only)

5. Loading State

Both components handle loading state to avoid:

  • Flash of redirect (flickering when changing pages)
  • Race condition (auth state not loaded yet)
if (isLoading) {
  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-50">
      <div className="text-center">
        <div className="animate-spin rounded-full h-12 w-12 
          border-b-2 border-indigo-600 mx-auto"
        />
        <p className="mt-4 text-gray-600">Authenticating...</p>
      </div>
    </div>
  );
}

6. Redirect After Login

LoginPage implementation

const LoginPage: React.FC = () => {
  const navigate = useNavigate();
  const location = useLocation();
  const { login, isLoading } = useAuthStore();

  const from = location.state?.from?.pathname || '/dashboard';

  const onSubmit = async (data: LoginFormData) => {
    try {
      await login(data);
      
      // Redirect to original page or /dashboard
      navigate(from, { replace: true });
      
      toast.success('Login successful!');
    } catch (error) {
      toast.error('Login failed!');
    }
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {/* ... form fields */}
    </form>
  );
};

Flow

  1. User accesses /bookings (protected)
  2. Redirect /login?from=/bookings
  3. Login successful
  4. Redirect to /bookings (original page)

7. Testing Route Protection

Test Case 1: ProtectedRoute - Unauthenticated

Given: User not logged in
When: Access /dashboard
Then: Redirect to /login
And: Save from=/dashboard in location state

Test Case 2: ProtectedRoute - Authenticated

Given: User logged in
When: Access /dashboard
Then: Display DashboardPage successfully

Test Case 3: AdminRoute - Not Admin

Given: User has role='customer'
When: Access /admin
Then: Redirect to / (homepage)

Test Case 4: AdminRoute - Is Admin

Given: User has role='admin'
When: Access /admin
Then: Display AdminLayout successfully

Test Case 5: Loading State

Given: Auth is initializing
When: isLoading === true
Then: Display loading spinner
And: No redirect


8. Security Best Practices

Implemented

  1. Client-side protection: ProtectedRoute & AdminRoute
  2. Token persistence: localStorage
  3. Role-based access: Check userInfo.role
  4. Location state: Save "from" to redirect to correct page
  5. Loading state: Avoid flash of redirect
  6. Replace navigation: Don't save redirect history

⚠️ Note

  • Client-side protection is not enough → Must have backend validation
  • API endpoints must check JWT + role
  • Backend middleware: auth, adminOnly
  • Never trust client-side role → Always verify on server

Backend Middleware Example

// server/src/middlewares/auth.js
const auth = async (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1];
  
  if (!token) {
    return res.status(401).json({ 
      message: 'Unauthorized' 
    });
  }
  
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = await User.findByPk(decoded.userId);
    next();
  } catch (error) {
    res.status(401).json({ message: 'Invalid token' });
  }
};

const adminOnly = (req, res, next) => {
  if (req.user.role !== 'admin') {
    return res.status(403).json({ 
      message: 'Forbidden: Admin only' 
    });
  }
  next();
};

// Usage
router.get('/admin/users', auth, adminOnly, getUsers);

9. Troubleshooting

Issue 1: Infinite redirect loop

Cause: ProtectedRoute check logic error
Solution: Ensure replace={true} in Navigate

Issue 2: Flash of redirect

Cause: Not handling loading state
Solution: Add check if (isLoading) before auth check

Issue 3: Lost location state

Cause: Not passing state={{ from: location }}
Solution: Always save location when redirecting

Issue 4: Admin can access but API fails

Cause: Backend doesn't verify role
Solution: Add adminOnly middleware on API routes


10. Summary

ProtectedRoute

  • Check isAuthenticated
  • Redirect /login if not logged in
  • Save location state to return
  • Handle loading state

AdminRoute

  • Check isAuthenticated first
  • Check userInfo.role === 'admin'
  • Redirect /login if not logged in
  • Redirect / if not admin
  • Handle loading state

Results

  • Protect all protected routes
  • Smooth UX, no flash
  • Role-based access works correctly
  • Good security (combined with backend validation)

Function 8 completed!