149 lines
3.9 KiB
JavaScript
149 lines
3.9 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
|
import { BrowserRouter as Router } from 'react-router-dom';
|
|
import io from 'socket.io-client';
|
|
|
|
import client from './shared/api/client';
|
|
import { checkSetup, getProfile, logout } from './shared/api/authApi';
|
|
import { LoadingSpinner } from './shared/components';
|
|
import AppProviders from './app/AppProviders';
|
|
import AppRoutes from './app/routes';
|
|
|
|
import './styles/base/App.css';
|
|
|
|
function AppContent() {
|
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
|
const [setupComplete, setSetupComplete] = useState(null);
|
|
const [socket, setSocket] = useState(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const forceLogout = React.useCallback(() => {
|
|
localStorage.removeItem('token');
|
|
setIsAuthenticated(false);
|
|
}, []);
|
|
|
|
// Global interceptor any 401/403 kicks the user back to login
|
|
useEffect(() => {
|
|
const interceptorId = client.interceptors.response.use(
|
|
(response) => response,
|
|
(error) => {
|
|
if (error.response?.status === 401 || error.response?.status === 403) {
|
|
forceLogout();
|
|
}
|
|
return Promise.reject(error);
|
|
}
|
|
);
|
|
return () => client.interceptors.response.eject(interceptorId);
|
|
}, [forceLogout]);
|
|
|
|
useEffect(() => {
|
|
// Check if setup is complete and validate any stored token
|
|
const loadSetupStatus = async () => {
|
|
try {
|
|
const response = await checkSetup();
|
|
// If we get a 200 response, setup is required
|
|
if (response.status === 200) {
|
|
setSetupComplete(false);
|
|
}
|
|
} catch (err) {
|
|
if (err.response?.status === 400) {
|
|
// 400 means setup is already complete
|
|
setSetupComplete(true);
|
|
} else {
|
|
// Assume setup is complete if we can't reach the endpoint
|
|
setSetupComplete(true);
|
|
}
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
// Validate the stored token against the server before trusting it
|
|
const validateToken = async () => {
|
|
const token = localStorage.getItem('token');
|
|
if (!token) return;
|
|
try {
|
|
await getProfile();
|
|
setIsAuthenticated(true);
|
|
} catch {
|
|
// Token is expired, revoked, or invalid, clear it
|
|
localStorage.removeItem('token');
|
|
setIsAuthenticated(false);
|
|
}
|
|
};
|
|
|
|
loadSetupStatus();
|
|
validateToken();
|
|
|
|
// Initialize WebSocket connection in production the frontend is served
|
|
// from the same origin as the backend, so no URL is needed.
|
|
const backendUrl = process.env.REACT_APP_BACKEND_URL || undefined;
|
|
const newSocket = io(backendUrl || window.location.origin, {
|
|
reconnection: true,
|
|
reconnectionDelay: 1000,
|
|
reconnectionDelayMax: 5000,
|
|
reconnectionAttempts: 5,
|
|
});
|
|
|
|
newSocket.on('connect', () => {
|
|
console.log('Connected to server');
|
|
});
|
|
|
|
setSocket(newSocket);
|
|
|
|
return () => newSocket.close();
|
|
}, []);
|
|
|
|
const handleLogin = (token) => {
|
|
localStorage.setItem('token', token);
|
|
setIsAuthenticated(true);
|
|
};
|
|
|
|
const handleLogout = async () => {
|
|
try {
|
|
const token = localStorage.getItem('token');
|
|
if (token) {
|
|
await logout();
|
|
}
|
|
} catch (err) {
|
|
// Logout even if the API call fails
|
|
console.error('Logout API error:', err);
|
|
}
|
|
forceLogout();
|
|
};
|
|
|
|
const handleSetupComplete = () => {
|
|
setSetupComplete(true);
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="loading-page">
|
|
<LoadingSpinner size="large" text="Initializing status page..." />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Router>
|
|
<AppRoutes
|
|
socket={socket}
|
|
isAuthenticated={isAuthenticated}
|
|
setupComplete={setupComplete}
|
|
onLogin={handleLogin}
|
|
onLogout={handleLogout}
|
|
onSetupComplete={handleSetupComplete}
|
|
/>
|
|
</Router>
|
|
);
|
|
}
|
|
|
|
function App() {
|
|
return (
|
|
<AppProviders>
|
|
<AppContent />
|
|
</AppProviders>
|
|
);
|
|
}
|
|
|
|
export default App;
|