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

485 lines
12 KiB
Markdown

# 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
```tsx
// 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
```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
```tsx
// 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
```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
```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
```tsx
// 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)
```tsx
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
```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 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
```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
### 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! ✅**