Version 0.4

This commit is contained in:
2026-04-16 23:08:23 +01:00
parent e011afbff1
commit 39eb818e7e
110 changed files with 18905 additions and 14 deletions

148
frontend/src/App.jsx Normal file
View File

@@ -0,0 +1,148 @@
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;