Files
Hotel-Booking/docs/ROUTE_PROTECTION.md
Iliyan Angelov 824eec6190 Hotel Booking
2025-11-16 14:19:13 +02:00

12 KiB

Route Protection Documentation

Chức năng 8: Phân quyền & Bảo vệ Route

Hệ thống sử dụng 2 component để bảo vệ các route:

  • ProtectedRoute: Yêu cầu user phải đăng nhập
  • AdminRoute: Yêu cầu user phải là Admin

1. ProtectedRoute

Mục đích

Bảo vệ các route yêu cầu authentication (đăng nhập).

Cách hoạt động

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

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

  // 1. Nếu đang loading → hiển thị spinner
  if (isLoading) {
    return <LoadingScreen />;
  }

  // 2. Nếu chưa đăng nhập → redirect /login
  if (!isAuthenticated) {
    return (
      <Navigate 
        to="/login" 
        state={{ from: location }}  // Lưu location để quay lại
        replace 
      />
    );
  }

  // 3. Đã đăng nhập → cho phép truy cập
  return <>{children}</>;
};

Sử dụng trong App.tsx

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

// Route yêu cầu đăng nhập
<Route 
  path="/dashboard" 
  element={
    <ProtectedRoute>
      <DashboardPage />
    </ProtectedRoute>
  } 
/>

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

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

Luồng hoạt động

  1. User chưa đăng nhập truy cập /dashboard
  2. ProtectedRoute kiểm tra isAuthenticated === false
  3. Redirect về /login và lưu state={{ from: '/dashboard' }}
  4. Sau khi login thành công, redirect về /dashboard

2. AdminRoute

Mục đích

Bảo vệ các route chỉ dành cho Admin (role-based access).

Cách hoạt động

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

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

  // 1. Nếu đang loading → hiển thị spinner
  if (isLoading) {
    return <LoadingScreen />;
  }

  // 2. Nếu chưa đăng nhập → redirect /login
  if (!isAuthenticated) {
    return (
      <Navigate 
        to="/login" 
        state={{ from: location }} 
        replace 
      />
    );
  }

  // 3. Nếu không phải admin → redirect /
  const isAdmin = userInfo?.role === 'admin';
  if (!isAdmin) {
    return <Navigate to="/" replace />;
  }

  // 4. Là admin → cho phép truy cập
  return <>{children}</>;
};

Sử dụng trong App.tsx

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

// Route chỉ dành cho Admin
<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>

Luồng hoạt động

Case 1: User chưa đăng nhập

  1. Truy cập /admin
  2. AdminRoute kiểm tra isAuthenticated === false
  3. Redirect về /login với state={{ from: '/admin' }}
  4. Sau login thành công → quay lại /admin
  5. AdminRoute kiểm tra lại role

Case 2: User đã đăng nhập nhưng không phải Admin

  1. Customer (role='customer') truy cập /admin
  2. AdminRoute kiểm tra isAuthenticated === true
  3. AdminRoute kiểm tra userInfo.role === 'customer' (không phải 'admin')
  4. Redirect về / (trang chủ)

Case 3: User là Admin

  1. Admin (role='admin') truy cập /admin
  2. AdminRoute kiểm tra isAuthenticated === true
  3. AdminRoute kiểm tra userInfo.role === 'admin'
  4. Cho phép truy cập

3. Cấu trúc Route trong App.tsx

function App() {
  return (
    <BrowserRouter>
      <Routes>
        {/* Public Routes - Không cần đăng nhập */}
        <Route path="/" element={<LayoutMain />}>
          <Route index element={<HomePage />} />
          <Route path="rooms" element={<RoomListPage />} />
          <Route path="about" element={<AboutPage />} />
        </Route>

        {/* Auth Routes - Không cần 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 - Yêu cầu đăng nhập */}
        <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 - Chỉ Admin */}
        <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. Tích hợp với 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: Quản trị viên (full access)
  • staff: Nhân viên (limited access)
  • customer: Khách hàng (customer features only)

5. Loading State

Cả 2 component đều xử lý loading state để tránh:

  • Flash of redirect (nhấp nháy khi chuyển trang)
  • Race condition (auth state chưa load xong)
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">Đang xác thực...</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 về page ban đầu hoặc /dashboard
      navigate(from, { replace: true });
      
      toast.success('Đăng nhập thành công!');
    } catch (error) {
      toast.error('Đăng nhập thất bại!');
    }
  };

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

Flow

  1. User truy cập /bookings (protected)
  2. Redirect /login?from=/bookings
  3. Login thành công
  4. Redirect về /bookings (page ban đầu)

7. Testing Route Protection

Test Case 1: ProtectedRoute - Unauthenticated

Given: User chưa đăng nhập
When: Truy cập /dashboard
Then: Redirect về /login
And: Lưu from=/dashboard trong location state

Test Case 2: ProtectedRoute - Authenticated

Given: User đã đăng nhập
When: Truy cập /dashboard
Then: Hiển thị DashboardPage thành công

Test Case 3: AdminRoute - Not Admin

Given: User có role='customer'
When: Truy cập /admin
Then: Redirect về / (trang chủ)

Test Case 4: AdminRoute - Is Admin

Given: User có role='admin'
When: Truy cập /admin
Then: Hiển thị AdminLayout thành công

Test Case 5: Loading State

Given: Auth đang initialize
When: isLoading === true
Then: Hiển thị loading spinner
And: Không redirect


8. Security Best Practices

Đã Implement

  1. Client-side protection: ProtectedRoute & AdminRoute
  2. Token persistence: localStorage
  3. Role-based access: Kiểm tra userInfo.role
  4. Location state: Lưu "from" để redirect về đúng page
  5. Loading state: Tránh flash của redirect
  6. Replace navigation: Không lưu lịch sử redirect

⚠️ Lưu Ý

  • Client-side protection không đủ → Phải có backend validation
  • API endpoints phải kiểm tra JWT + role
  • Middleware backend: 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

Vấn đề 1: Infinite redirect loop

Nguyên nhân: ProtectedRoute check sai logic
Giải pháp: Đảm bảo replace={true} trong Navigate

Vấn đề 2: Flash of redirect

Nguyên nhân: Không handle loading state
Giải pháp: Thêm check if (isLoading) trước check auth

Vấn đề 3: Lost location state

Nguyên nhân: Không pass state={{ from: location }}
Giải pháp: Luôn lưu location khi redirect

Vấn đề 4: Admin có thể truy cập nhưng API fail

Nguyên nhân: Backend không verify role
Giải pháp: Thêm middleware adminOnly trên API routes


10. Summary

ProtectedRoute

  • Kiểm tra isAuthenticated
  • Redirect /login nếu chưa đăng nhập
  • Lưu location state để quay lại
  • Handle loading state

AdminRoute

  • Kiểm tra isAuthenticated trước
  • Kiểm tra userInfo.role === 'admin'
  • Redirect /login nếu chưa đăng nhập
  • Redirect / nếu không phải admin
  • Handle loading state

Kết quả

  • Bảo vệ toàn bộ protected routes
  • UX mượt mà, không flash
  • Role-based access hoạt động chính xác
  • Security tốt (kết hợp backend validation)

Chức năng 8 hoàn thành!