# 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 ```tsx // File: client/src/components/auth/ProtectedRoute.tsx const ProtectedRoute: React.FC = ({ children }) => { const location = useLocation(); const { isAuthenticated, isLoading } = useAuthStore(); // 1. Nếu đang loading → hiển thị spinner if (isLoading) { return ; } // 2. Nếu chưa đăng nhập → redirect /login if (!isAuthenticated) { return ( ); } // 3. Đã đăng nhập → cho phép truy cập return <>{children}; }; ``` ### Sử dụng trong App.tsx ```tsx import { ProtectedRoute } from './components/auth'; // Route yêu cầu đăng nhập } /> } /> } /> ``` ### 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 ```tsx // File: client/src/components/auth/AdminRoute.tsx const AdminRoute: React.FC = ({ children }) => { const location = useLocation(); const { isAuthenticated, userInfo, isLoading } = useAuthStore(); // 1. Nếu đang loading → hiển thị spinner if (isLoading) { return ; } // 2. Nếu chưa đăng nhập → redirect /login if (!isAuthenticated) { return ( ); } // 3. Nếu không phải admin → redirect / const isAdmin = userInfo?.role === 'admin'; if (!isAdmin) { return ; } // 4. Là admin → cho phép truy cập return <>{children}; }; ``` ### Sử dụng trong App.tsx ```tsx import { AdminRoute } from './components/auth'; // Route chỉ dành cho Admin } > } /> } /> } /> } /> } /> ``` ### 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 ```tsx function App() { return ( {/* Public Routes - Không cần đăng nhập */} }> } /> } /> } /> {/* Auth Routes - Không cần layout */} } /> } /> } /> } /> {/* Protected Routes - Yêu cầu đăng nhập */} }> } /> } /> } /> {/* Admin Routes - Chỉ Admin */} } > } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> {/* 404 Route */} } /> ); } ``` --- ## 4. Tích hợp với Zustand Store ### useAuthStore State ```tsx // File: client/src/store/useAuthStore.ts const useAuthStore = create((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) ```tsx if (isLoading) { return (

Đang xác thực...

); } ``` --- ## 6. Redirect After Login ### LoginPage implementation ```tsx 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 fields */}
); }; ``` ### 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 ```javascript // 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! ✅**