12 KiB
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
// 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
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
- User not logged in accesses
/dashboard - ProtectedRoute checks
isAuthenticated === false - Redirect to
/loginand savestate={{ from: '/dashboard' }} - After successful login, redirect to
/dashboard
2. AdminRoute
Purpose
Protects routes for Admin only (role-based access).
How It Works
// 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
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
- Access
/admin - AdminRoute checks
isAuthenticated === false - Redirect to
/loginwithstate={{ from: '/admin' }} - After successful login → return to
/admin - AdminRoute checks role again
Case 2: User logged in but not Admin
- Customer (role='customer') accesses
/admin - AdminRoute checks
isAuthenticated === true - AdminRoute checks
userInfo.role === 'customer'(not 'admin') - Redirect to
/(homepage)
Case 3: User is Admin
- Admin (role='admin') accesses
/admin - AdminRoute checks
isAuthenticated === true - AdminRoute checks
userInfo.role === 'admin'✅ - Allow access
3. Route Structure in App.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
// 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)
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
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
- User accesses
/bookings(protected) - Redirect
/login?from=/bookings - Login successful
- 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
- Client-side protection: ProtectedRoute & AdminRoute
- Token persistence: localStorage
- Role-based access: Check userInfo.role
- Location state: Save "from" to redirect to correct page
- Loading state: Avoid flash of redirect
- 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
// 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
/loginif not logged in - ✅ Save location state to return
- ✅ Handle loading state
AdminRoute
- ✅ Check
isAuthenticatedfirst - ✅ Check
userInfo.role === 'admin' - ✅ Redirect
/loginif 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! ✅