Authentication & Route Protection
Route Protection Overview
StockEase uses role-based access control (RBAC) to protect routes. Different pages require different authentication levels.
Access Levels
1. Public Routes (No Authentication Required)
<Route path="/" element={<HomePage />} />
<Route path="/login" element={<LoginPage />} />Pages:
- HomePage
- LoginPage
Behavior:
- Accessible to everyone
- Logged-in users should be redirected away from LoginPage
2. Protected Routes (Login Required)
<Route
path="/user"
element={<ProtectedRoute component={UserDashboard} />}
/>Pages:
- UserDashboard
- SearchProductPage
- ListStockPage
Requirements:
- User must be authenticated
- Any logged-in user (admin or regular user) can access
Redirect behavior:
- Not authenticated → Redirect to
/login - Authenticated → Allow access
3. Admin-Only Routes (Admin Role Required)
<Route
path="/admin"
element={<ProtectedRoute component={AdminDashboard} requiredRole="admin" />}
/>Pages:
- AdminDashboard
- AddProductPage
- DeleteProductPage
- ChangeProductDetailsPage
Requirements:
- User must be authenticated
- User must have "admin" role
Redirect behavior:
- Not authenticated → Redirect to
/login - Not admin → Redirect to
/unauthorizedor UserDashboard
ProtectedRoute Component
Implementation
/**
* ProtectedRoute.tsx
*
* Higher-order component wrapper for protecting routes based on
* authentication status and user role.
*/
import React from 'react';
import { Navigate } from 'react-router-dom';
import { useSelector } from 'react-redux';
interface ProtectedRouteProps {
component: React.ComponentType<any>;
requiredRole?: 'admin' | 'user';
}
export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
component: Component,
requiredRole,
}) => {
// Get authentication state from Redux
const { user, isAuthenticated } = useSelector(
state => state.auth
);
// Step 1: Check if user is authenticated
if (!isAuthenticated) {
// Not logged in - redirect to login
return <Navigate to="/login" replace />;
}
// Step 2: Check if user has required role (if specified)
if (requiredRole && user?.role !== requiredRole) {
// Wrong role - redirect to unauthorized page
return <Navigate to="/unauthorized" replace />;
}
// Step 3: All checks passed - render component
return <Component />;
};
export default ProtectedRoute;Usage in Routes
// Protected route - all authenticated users
<Route
path="/user"
element={<ProtectedRoute component={UserDashboard} />}
/>
// Admin-only route - requires admin role
<Route
path="/admin"
element={
<ProtectedRoute
component={AdminDashboard}
requiredRole="admin"
/>
}
/>
// Admin-only with role check
<Route
path="/add-product"
element={
<ProtectedRoute
component={AddProductPage}
requiredRole="admin"
/>
}
/>In-Page Authentication Check
Some pages implement their own authentication check as an additional safety layer:
/**
* UserDashboard.tsx
*
* Page performs its own authentication check as a safety measure,
* even though the route is protected.
*/
const UserDashboard: React.FC = () => {
const { user, isAuthenticated } = useSelector(state => state.auth);
// Safety check: verify authentication
if (!isAuthenticated) {
return <Navigate to="/login" />;
}
// Rest of component...
};Why Double-Check?
- Defense in depth: Extra safety layer
- Direct page access: If someone navigates directly (bookmarks)
- Session expiration: Catch expired sessions mid-page
Authentication Flow
Complete Login Flow
sequenceDiagram
participant User as User
participant Login as LoginPage
participant Service as AuthService
participant API as Backend API
participant Redux as Redux Store
participant Router as Router
participant Protected as ProtectedRoute
User->>Login: Enter email + password
User->>Login: Click login button
Login->>Service: login(email, password)
Service->>API: POST /api/auth/login
API->>API: Verify credentials
API-->>Service: {token, userId, role}
Service->>Redux: dispatch(setUser{token, role, userId})
Redux->>Redux: Update auth state
Redux-->>Service: Updated state
Service-->>Login: Success
Login->>Router: Navigate to /user or /admin
Router->>Protected: Check route protection
Protected->>Redux: Get auth state
Redux-->>Protected: {isAuthenticated, user}
Protected->>Protected: Verify authenticated + role
Protected-->>Router: Access granted
Router->>Router: Render target component
Access Attempt Flow
graph TD
A["User navigates to /admin"]
B["Router matches route"]
C["ProtectedRoute component"]
D["Get auth state from Redux"]
E{"isAuthenticated?"}
F["Render component"]
G{"role == 'admin'?"}
H["Navigate to /login"]
I["Navigate to /unauthorized"]
A --> B
B --> C
C --> D
D --> E
E -->|No| H
E -->|Yes| G
G -->|Yes| F
G -->|No| I
User Roles
Admin Role
interface AdminUser {
userId: string;
email: string;
role: 'admin'; // Special role
permissions: [
'create_product',
'edit_product',
'delete_product',
'view_all_products',
'manage_users' // optional
];
}Access: All pages including admin-only pages
Capabilities:
- Add products
- Edit products
- Delete products
- View all products
- Access AdminDashboard
User Role
interface RegularUser {
userId: string;
email: string;
role: 'user'; // Regular user role
permissions: [
'view_own_products',
'search_products',
'list_products'
];
}Access: Protected pages only
Capabilities:
- View own products (UserDashboard)
- Search products (SearchProductPage)
- View inventory list (ListStockPage)
- Cannot create/edit/delete
Token Management
Saving Auth Token
/**
* After successful login, store token in localStorage
*/
const handleLoginSuccess = (response) => {
const { token, role, userId } = response;
// Store token securely
localStorage.setItem('authToken', token);
localStorage.setItem('userRole', role);
localStorage.setItem('userId', userId);
// Update Redux
dispatch({
type: 'SET_USER',
payload: { token, role, userId }
});
};Using Auth Token in API Calls
/**
* Add token to all API requests
*/
// In apiClient.ts
const apiClient = axios.create({
baseURL: process.env.VITE_API_BASE_URL
});
apiClient.interceptors.request.use(config => {
const token = localStorage.getItem('authToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// All API calls automatically include token
await apiClient.get('/api/products');Token Expiration Handling
// In apiClient interceptor
apiClient.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 401) {
// Token expired or invalid
localStorage.removeItem('authToken');
dispatch({ type: 'LOGOUT' });
window.location.href = '/login';
}
return Promise.reject(error);
}
);Logout Flow
const handleLogout = () => {
// 1. Clear Redux store
dispatch({ type: 'LOGOUT' });
// 2. Clear localStorage
localStorage.removeItem('authToken');
localStorage.removeItem('userRole');
localStorage.removeItem('userId');
// 3. Navigate to login
navigate('/login');
};Unauthorized Page
Component for Access Denied
/**
* UnauthorizedPage.tsx
* Shown when user lacks required role
*/
const UnauthorizedPage: React.FC = () => {
const { t } = useTranslation();
return (
<div className="unauthorized">
<h1>{t('error.unauthorized')}</h1>
<p>{t('error.insufficient_permissions')}</p>
<div className="actions">
<button onClick={() => navigate('/user')}>
{t('buttons.go_to_dashboard')}
</button>
<button onClick={() => navigate(-1)}>
{t('buttons.go_back')}
</button>
</div>
</div>
);
};Route for Unauthorized
<Route path="/unauthorized" element={<UnauthorizedPage />} />Session Management
Session Persistence
/**
* App.tsx - On app load, restore session
*/
useEffect(() => {
const initializeAuth = () => {
const token = localStorage.getItem('authToken');
const role = localStorage.getItem('userRole');
const userId = localStorage.getItem('userId');
if (token && userId) {
// Restore session
dispatch({
type: 'SET_USER',
payload: { token, role, userId }
});
}
};
initializeAuth();
}, [dispatch]);Session Timeout (Optional)
/**
* Logout user after inactivity period
*/
useEffect(() => {
const TIMEOUT = 30 * 60 * 1000; // 30 minutes
let timeoutId: NodeJS.Timeout;
const resetTimeout = () => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
dispatch({ type: 'LOGOUT' });
navigate('/login');
}, TIMEOUT);
};
window.addEventListener('mousemove', resetTimeout);
window.addEventListener('keypress', resetTimeout);
resetTimeout();
return () => {
window.removeEventListener('mousemove', resetTimeout);
window.removeEventListener('keypress', resetTimeout);
clearTimeout(timeoutId);
};
}, [dispatch, navigate]);Security Best Practices
âś… DO:
// Store token securely
localStorage.setItem('authToken', token);
// Check authentication on protected pages
if (!isAuthenticated) return <Navigate to="/login" />;
// Include token in API requests
headers: { Authorization: `Bearer ${token}` }
// Clear token on logout
localStorage.removeItem('authToken');
// Verify role for admin pages
if (user.role !== 'admin') return <Navigate to="/unauthorized" />;❌ DON'T:
// Don't expose token in URLs
window.location = `/dashboard?token=${token}`;
// Don't hardcode credentials
const password = 'admin123';
// Don't skip authentication checks
<Route path="/admin" element={<AdminDashboard />} />
// Don't store sensitive data in Redux alone
// Always persist to secure storage
// Don't pass credentials in request body for auth
// Use headers and tokens insteadRelated Documentation
- Overview - Routing structure and pages
- Page Components - Individual page details
- Page Lifecycle - Component patterns and structure
- Performance - Optimization techniques
- Testing - Testing protected routes
Last Updated: November 2025