485 lines
12 KiB
Markdown
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! ✅**
|