diff --git a/src/App.jsx b/src/App.jsx index 65a4ae8..4ec956d 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -33,10 +33,28 @@ const API_BASE = import.meta.env.VITE_API_BASE_URL || // Secure API fetch wrapper const apiFetch = (url, options = {}) => { - return fetch(url, options); + const token = localStorage.getItem('rmm_token'); + if (token) { + options.headers = { + ...options.headers, + 'Authorization': `Bearer ${token}` + }; + } + return fetch(url, options).then(response => { + if (response.status === 401) { + localStorage.removeItem('rmm_token'); + window.dispatchEvent(new Event('rmm_unauthorized')); + } + return response; + }); }; function App() { + const [token, setToken] = useState(localStorage.getItem('rmm_token') || ''); + const [loginUsername, setLoginUsername] = useState(''); + const [loginPassword, setLoginPassword] = useState(''); + const [loginError, setLoginError] = useState(''); + const [activeTab, setActiveTab] = useState('dashboard'); // 'dashboard' | 'editor' const [clients, setClients] = useState({}); const [telemetry, setTelemetry] = useState({}); @@ -112,13 +130,55 @@ function App() { }; useEffect(() => { + const handleUnauthorized = () => { + setToken(''); + }; + window.addEventListener('rmm_unauthorized', handleUnauthorized); + return () => window.removeEventListener('rmm_unauthorized', handleUnauthorized); + }, []); + + useEffect(() => { + if (!token) return; + fetchWhitelist(); syncFleetData(); // Set up rapid 3-second fleet synchronization loop const syncInterval = setInterval(syncFleetData, 3000); return () => clearInterval(syncInterval); - }, []); + }, [token]); + + const handleLogin = async (e) => { + e.preventDefault(); + setLoginError(''); + try { + const response = await fetch(`${API_BASE}/api/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username: loginUsername, password: loginPassword }) + }); + const data = await response.json(); + if (response.ok) { + localStorage.setItem('rmm_token', data.token); + setToken(data.token); + setLoginPassword(''); + showToast('Login successful!', 'success'); + } else { + setLoginError(data.detail || 'Login failed. Invalid username or password.'); + } + } catch (err) { + setLoginError('Cannot connect to RMM backend server.'); + } + }; + + const handleLogout = () => { + localStorage.removeItem('rmm_token'); + setToken(''); + setClients({}); + setTelemetry({}); + setLogs({}); + showToast('Logged out successfully.', 'success'); + }; // Scroll terminal logs to bottom automatically when new actions populate, // but only if the user is already looking at the latest logs (near the bottom)! @@ -260,6 +320,70 @@ function App() { return matchesSearch && matchesFilter; }); + if (!token) { + return ( +
Operator Authentication Required
+