Compare commits
1 Commits
Author | SHA1 | Date |
---|---|---|
Steve White | 9861deb49f |
|
@ -16,6 +16,7 @@
|
||||||
"@testing-library/react": "^13.4.0",
|
"@testing-library/react": "^13.4.0",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
|
"jwt-decode": "^4.0.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-router-dom": "^6.26.2",
|
"react-router-dom": "^6.26.2",
|
||||||
|
@ -13779,6 +13780,15 @@
|
||||||
"node": ">=4.0"
|
"node": ">=4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jwt-decode": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/keyv": {
|
"node_modules/keyv": {
|
||||||
"version": "4.5.4",
|
"version": "4.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
"@testing-library/react": "^13.4.0",
|
"@testing-library/react": "^13.4.0",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
|
"jwt-decode": "^4.0.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-router-dom": "^6.26.2",
|
"react-router-dom": "^6.26.2",
|
||||||
|
|
48
src/App.js
48
src/App.js
|
@ -1,14 +1,14 @@
|
||||||
// src/App.js
|
// src/App.js
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { BrowserRouter as Router, Route, Routes, Navigate } from 'react-router-dom';
|
import { BrowserRouter as Router, Route, Routes, Navigate, useNavigate } from 'react-router-dom';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { jwtDecode } from 'jwt-decode';
|
||||||
import Login from './components/Login';
|
import Login from './components/Login';
|
||||||
import Boxes from './components/Boxes';
|
import Boxes from './components/Boxes';
|
||||||
import Items from './components/Items';
|
import Items from './components/Items';
|
||||||
import Navbar from './components/Navbar'; // Correct import here
|
import Navbar from './components/Navbar';
|
||||||
import Admin from './components/Admin'; // Correct import here
|
import Admin from './components/Admin';
|
||||||
import { createContext } from 'react';
|
import { createContext } from 'react';
|
||||||
import ErrorBoundary from './components/ErrorBoundary';
|
|
||||||
import './styles.css'
|
import './styles.css'
|
||||||
|
|
||||||
export const AppContext = createContext();
|
export const AppContext = createContext();
|
||||||
|
@ -16,6 +16,40 @@ export const PRIMARY_COLOR = '#333';
|
||||||
export const SECONDARY_COLOR = '#ffffff';
|
export const SECONDARY_COLOR = '#ffffff';
|
||||||
export const BACKGROUND_COLOR = '#dddddd';
|
export const BACKGROUND_COLOR = '#dddddd';
|
||||||
|
|
||||||
|
// New component for token expiration check
|
||||||
|
function TokenExpirationCheck({ setToken }) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkTokenExpiration = () => {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (!token) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const decodedToken = jwtDecode(token);
|
||||||
|
if (Date.now() >= decodedToken.exp * 1000) {
|
||||||
|
setToken(null);
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
navigate('/api/v1/login');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
navigate('/api/v1/login');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('focus', checkTokenExpiration);
|
||||||
|
const interval = setInterval(checkTokenExpiration, 60000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('focus', checkTokenExpiration);
|
||||||
|
clearInterval(interval);
|
||||||
|
};
|
||||||
|
}, [navigate, setToken]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [token, setToken] = useState(localStorage.getItem('token'));
|
const [token, setToken] = useState(localStorage.getItem('token'));
|
||||||
|
|
||||||
|
@ -27,18 +61,17 @@ function App() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppContext.Provider value={{ token, setToken }}>
|
<AppContext.Provider value={{ token, setToken }}>
|
||||||
<ErrorBoundary>
|
|
||||||
<Router>
|
<Router>
|
||||||
|
<TokenExpirationCheck setToken={setToken} />
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<AppRoutes token={token} setToken={setToken} />
|
<AppRoutes token={token} setToken={setToken} />
|
||||||
</Router>
|
</Router>
|
||||||
</ErrorBoundary>
|
|
||||||
</AppContext.Provider>
|
</AppContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AppRoutes({ token, setToken }) {
|
function AppRoutes({ token, setToken }) {
|
||||||
const { id } = useParams(); // Move useParams here
|
const { id } = useParams();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -65,4 +98,5 @@ function AppRoutes({ token, setToken }) {
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
|
@ -18,7 +18,8 @@ import {
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
Alert,
|
Alert,
|
||||||
CircularProgress
|
CircularProgress,
|
||||||
|
Skeleton
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { Delete as DeleteIcon, Edit as EditIcon } from '@mui/icons-material';
|
import { Delete as DeleteIcon, Edit as EditIcon } from '@mui/icons-material';
|
||||||
import { useParams, useLocation } from 'react-router-dom';
|
import { useParams, useLocation } from 'react-router-dom';
|
||||||
|
@ -51,6 +52,26 @@ const Item = React.memo(({ item, onDelete, onEdit, itemImages }) => (
|
||||||
</TableRow>
|
</TableRow>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
const LoadingSkeleton = () => (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Skeleton variant="circular" width={40} height={40} />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Skeleton variant="text" width={100} />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Skeleton variant="text" width="80%" />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Box display="flex" gap={1}>
|
||||||
|
<Skeleton variant="circular" width={30} height={30} />
|
||||||
|
<Skeleton variant="circular" width={30} height={30} />
|
||||||
|
</Box>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
|
||||||
export default function Items({ token }) {
|
export default function Items({ token }) {
|
||||||
const { id: boxId } = useParams();
|
const { id: boxId } = useParams();
|
||||||
const [items, setItems] = useState([]);
|
const [items, setItems] = useState([]);
|
||||||
|
@ -294,7 +315,12 @@ export default function Items({ token }) {
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{filteredItems.map((item) => (
|
{loadingItems ? (
|
||||||
|
[...Array(5)].map((_, i) => (
|
||||||
|
<LoadingSkeleton key={i} />
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
filteredItems.map((item) => (
|
||||||
<Item
|
<Item
|
||||||
key={item.ID}
|
key={item.ID}
|
||||||
item={item}
|
item={item}
|
||||||
|
@ -302,7 +328,8 @@ export default function Items({ token }) {
|
||||||
onDelete={handleDeleteItem}
|
onDelete={handleDeleteItem}
|
||||||
onEdit={handleEditItem}
|
onEdit={handleEditItem}
|
||||||
/>
|
/>
|
||||||
))}
|
))
|
||||||
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
// src/utils/errorHandler.js
|
// src/services/errorHandler.js
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { jwtDecode } from 'jwt-decode';
|
||||||
|
|
||||||
export class ApiError extends Error {
|
export class ApiError extends Error {
|
||||||
constructor(message, status, details = {}) {
|
constructor(message, status, details = {}) {
|
||||||
|
@ -9,21 +10,80 @@ export class ApiError extends Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enhanced API client with error handling
|
|
||||||
export const createApiClient = () => {
|
export const createApiClient = () => {
|
||||||
const client = axios.create({
|
const client = axios.create({
|
||||||
baseURL: process.env.REACT_APP_API_URL
|
baseURL: process.env.REACT_APP_API_URL,
|
||||||
|
headers: {
|
||||||
|
'X-Content-Type-Options': 'nosniff',
|
||||||
|
'X-Frame-Options': 'DENY',
|
||||||
|
'X-XSS-Protection': '1; mode=block'
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add request interceptor for token validation
|
||||||
|
client.interceptors.request.use(async (config) => {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (!token) return config;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const decodedToken = jwtDecode(token);
|
||||||
|
const currentTime = Date.now() / 1000;
|
||||||
|
|
||||||
|
if (decodedToken.exp < currentTime) {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
window.location.href = '/api/v1/login';
|
||||||
|
throw new ApiError('Session expired', 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh token if expiring soon
|
||||||
|
if (decodedToken.exp - currentTime < 300) {
|
||||||
|
try {
|
||||||
|
const response = await axios.post('/api/v1/refresh-token', { token });
|
||||||
|
localStorage.setItem('token', response.data.token);
|
||||||
|
config.headers.Authorization = `Bearer ${response.data.token}`;
|
||||||
|
} catch (error) {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
window.location.href = '/api/v1/login';
|
||||||
|
throw new ApiError('Failed to refresh token', 401);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
} catch (error) {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
window.location.href = '/api/v1/login';
|
||||||
|
throw new ApiError('Invalid token', 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add CSRF token if available
|
||||||
|
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
|
||||||
|
if (csrfToken) {
|
||||||
|
config.headers['X-CSRF-Token'] = csrfToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enhanced response interceptor
|
||||||
client.interceptors.response.use(
|
client.interceptors.response.use(
|
||||||
response => response,
|
response => response,
|
||||||
error => {
|
error => {
|
||||||
if (error.response?.status === 401) {
|
if (error.response?.status === 401) {
|
||||||
localStorage.removeItem('token');
|
localStorage.removeItem('token');
|
||||||
window.location.href = '/login';
|
window.location.href = '/api/v1/login';
|
||||||
return Promise.reject(new ApiError('Session expired', 401));
|
return Promise.reject(new ApiError('Session expired', 401));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle rate limiting
|
||||||
|
if (error.response?.status === 429) {
|
||||||
|
return Promise.reject(new ApiError('Too many requests. Please try again later.', 429));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle server errors
|
||||||
|
if (error.response?.status >= 500) {
|
||||||
|
return Promise.reject(new ApiError('Server error. Please try again later.', error.response.status));
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.reject(new ApiError(
|
return Promise.reject(new ApiError(
|
||||||
error.response?.data?.message || 'An error occurred',
|
error.response?.data?.message || 'An error occurred',
|
||||||
error.response?.status,
|
error.response?.status,
|
||||||
|
|
Loading…
Reference in New Issue