This commit is contained in:
Iliyan Angelov
2025-11-16 15:12:43 +02:00
parent 824eec6190
commit 93d4c1df80
54 changed files with 1606 additions and 1612 deletions

View File

@@ -1,19 +1,19 @@
# Route Protection Documentation
## Chức năng 8: Phân quyền & Bảo vệ Route
## Function 8: Authorization & Route Protection
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
The system uses 2 components to protect routes:
- **ProtectedRoute**: Requires user to be logged in
- **AdminRoute**: Requires user to be Admin
---
## 1. ProtectedRoute
### Mục đích
Bảo vệ các route yêu cầu authentication (đăng nhập).
### Purpose
Protects routes requiring authentication (login).
### Cách hoạt động
### How It Works
```tsx
// File: client/src/components/auth/ProtectedRoute.tsx
@@ -23,32 +23,32 @@ const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
const location = useLocation();
const { isAuthenticated, isLoading } = useAuthStore();
// 1. Nếu đang loading → hiển thị spinner
// 1. If loading → display spinner
if (isLoading) {
return <LoadingScreen />;
}
// 2. Nếu chưa đăng nhập → redirect /login
// 2. If not logged in → redirect /login
if (!isAuthenticated) {
return (
<Navigate
to="/login"
state={{ from: location }} // Lưu location để quay lại
state={{ from: location }} // Save location to return later
replace
/>
);
}
// 3. Đã đăng nhập → cho phép truy cập
// 3. Logged in → allow access
return <>{children}</>;
};
```
### Sử dụng trong App.tsx
### Usage in App.tsx
```tsx
import { ProtectedRoute } from './components/auth';
// Route yêu cầu đăng nhập
// Route requiring login
<Route
path="/dashboard"
element={
@@ -77,20 +77,20 @@ import { ProtectedRoute } from './components/auth';
/>
```
### 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`
### 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
### Mục đích
Bảo vệ các route chỉ dành cho Admin (role-based access).
### Purpose
Protects routes for Admin only (role-based access).
### Cách hoạt động
### How It Works
```tsx
// File: client/src/components/auth/AdminRoute.tsx
@@ -100,12 +100,12 @@ const AdminRoute: React.FC<AdminRouteProps> = ({
const location = useLocation();
const { isAuthenticated, userInfo, isLoading } = useAuthStore();
// 1. Nếu đang loading → hiển thị spinner
// 1. If loading → display spinner
if (isLoading) {
return <LoadingScreen />;
}
// 2. Nếu chưa đăng nhập → redirect /login
// 2. If not logged in → redirect /login
if (!isAuthenticated) {
return (
<Navigate
@@ -116,22 +116,22 @@ const AdminRoute: React.FC<AdminRouteProps> = ({
);
}
// 3. Nếu không phải admin → redirect /
// 3. If not admin → redirect /
const isAdmin = userInfo?.role === 'admin';
if (!isAdmin) {
return <Navigate to="/" replace />;
}
// 4. admin → cho phép truy cập
// 4. Is admin → allow access
return <>{children}</>;
};
```
### Sử dụng trong App.tsx
### Usage in App.tsx
```tsx
import { AdminRoute } from './components/auth';
// Route chỉ dành cho Admin
// Route for Admin only
<Route
path="/admin"
element={
@@ -148,50 +148,50 @@ import { AdminRoute } from './components/auth';
</Route>
```
### Luồng hoạt động
### Flow
#### 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 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 đã đă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 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 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
#### 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. Cấu trúc Route trong App.tsx
## 3. Route Structure in App.tsx
```tsx
function App() {
return (
<BrowserRouter>
<Routes>
{/* Public Routes - Không cần đăng nhập */}
{/* 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 - Không cần layout */}
{/* 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 - Yêu cầu đăng nhập */}
{/* Protected Routes - Login required */}
<Route path="/" element={<LayoutMain />}>
<Route
path="dashboard"
@@ -219,7 +219,7 @@ function App() {
/>
</Route>
{/* Admin Routes - Chỉ Admin */}
{/* Admin Routes - Admin only */}
<Route
path="/admin"
element={
@@ -251,7 +251,7 @@ function App() {
---
## 4. Tích hợp với Zustand Store
## 4. Integration with Zustand Store
### useAuthStore State
```tsx
@@ -286,17 +286,17 @@ const useAuthStore = create<AuthStore>((set) => ({
```
### 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)
- **admin**: Administrator (full access)
- **staff**: Staff (limited access)
- **customer**: Customer (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)
Both components handle loading state to avoid:
- Flash of redirect (flickering when changing pages)
- Race condition (auth state not loaded yet)
```tsx
if (isLoading) {
@@ -306,7 +306,7 @@ if (isLoading) {
<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>
<p className="mt-4 text-gray-600">Authenticating...</p>
</div>
</div>
);
@@ -330,12 +330,12 @@ const LoginPage: React.FC = () => {
try {
await login(data);
// Redirect về page ban đầu hoặc /dashboard
// Redirect to original page or /dashboard
navigate(from, { replace: true });
toast.success('Đăng nhập thành công!');
toast.success('Login successful!');
} catch (error) {
toast.error('Đăng nhập thất bại!');
toast.error('Login failed!');
}
};
@@ -348,58 +348,58 @@ const LoginPage: React.FC = () => {
```
### Flow
1. User truy cập `/bookings` (protected)
1. User accesses `/bookings` (protected)
2. Redirect `/login?from=/bookings`
3. Login thành công
4. Redirect về `/bookings` (page ban đầu)
3. Login successful
4. Redirect to `/bookings` (original page)
---
## 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
**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 đã đăng nhập
**When**: Truy cập `/dashboard`
**Then**: Hiển thị DashboardPage thành công
**Given**: User logged in
**When**: Access `/dashboard`
**Then**: Display DashboardPage successfully
### Test Case 3: AdminRoute - Not Admin
**Given**: User role='customer'
**When**: Truy cập `/admin`
**Then**: Redirect về `/` (trang chủ)
**Given**: User has role='customer'
**When**: Access `/admin`
**Then**: Redirect to `/` (homepage)
### Test Case 4: AdminRoute - Is Admin
**Given**: User role='admin'
**When**: Truy cập `/admin`
**Then**: Hiển thị AdminLayout thành công
**Given**: User has role='admin'
**When**: Access `/admin`
**Then**: Display AdminLayout successfully
### Test Case 5: Loading State
**Given**: Auth đang initialize
**Given**: Auth is initializing
**When**: isLoading === true
**Then**: Hiển thị loading spinner
**And**: Không redirect
**Then**: Display loading spinner
**And**: No redirect
---
## 8. Security Best Practices
### ✅ Đã Implement
### ✅ Implemented
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
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
### ⚠️ 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`
### ⚠️ 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
@@ -440,45 +440,45 @@ 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
### Issue 1: Infinite redirect loop
**Cause**: ProtectedRoute check logic error
**Solution**: Ensure `replace={true}` in 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
### Issue 2: Flash of redirect
**Cause**: Not handling loading state
**Solution**: Add check `if (isLoading)` before auth check
### 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
### Issue 3: Lost location state
**Cause**: Not passing `state={{ from: location }}`
**Solution**: Always save location when redirecting
### 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
### Issue 4: Admin can access but API fails
**Cause**: Backend doesn't verify role
**Solution**: Add `adminOnly` middleware on 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
-Check `isAuthenticated`
- ✅ Redirect `/login` if not logged in
-Save location state to return
- ✅ 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
-Check `isAuthenticated` first
-Check `userInfo.role === 'admin'`
- ✅ Redirect `/login` if not logged in
- ✅ Redirect `/` if not 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)
### Results
- Protect all protected routes
- Smooth UX, no flash
- Role-based access works correctly
- Good security (combined with backend validation)
---
**Chức năng 8 hoàn thành! ✅**
**Function 8 completed! ✅**