Compare commits
2 Commits
435c004760
...
57d395d6d7
| Author | SHA1 | Date | |
|---|---|---|---|
| 57d395d6d7 | |||
| ac094965b5 |
@@ -226,11 +226,6 @@ class SQLiteRepository(BaseRepository):
|
|||||||
select(func.count(func.distinct(Log.attacker_ip)))
|
select(func.count(func.distinct(Log.attacker_ip)))
|
||||||
)
|
)
|
||||||
).scalar() or 0
|
).scalar() or 0
|
||||||
active_deckies = (
|
|
||||||
await session.execute(
|
|
||||||
select(func.count(func.distinct(Log.decky)))
|
|
||||||
)
|
|
||||||
).scalar() or 0
|
|
||||||
|
|
||||||
_state = await asyncio.to_thread(load_state)
|
_state = await asyncio.to_thread(load_state)
|
||||||
deployed_deckies = len(_state[0].deckies) if _state else 0
|
deployed_deckies = len(_state[0].deckies) if _state else 0
|
||||||
@@ -238,7 +233,7 @@ class SQLiteRepository(BaseRepository):
|
|||||||
return {
|
return {
|
||||||
"total_logs": total_logs,
|
"total_logs": total_logs,
|
||||||
"unique_attackers": unique_attackers,
|
"unique_attackers": unique_attackers,
|
||||||
"active_deckies": active_deckies,
|
"active_deckies": deployed_deckies,
|
||||||
"deployed_deckies": deployed_deckies,
|
"deployed_deckies": deployed_deckies,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,15 +9,30 @@ import Attackers from './components/Attackers';
|
|||||||
import Config from './components/Config';
|
import Config from './components/Config';
|
||||||
import Bounty from './components/Bounty';
|
import Bounty from './components/Bounty';
|
||||||
|
|
||||||
|
function isTokenValid(token: string): boolean {
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(atob(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')));
|
||||||
|
return typeof payload.exp === 'number' && payload.exp * 1000 > Date.now();
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getValidToken(): string | null {
|
||||||
|
const stored = localStorage.getItem('token');
|
||||||
|
if (stored && isTokenValid(stored)) return stored;
|
||||||
|
if (stored) localStorage.removeItem('token');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [token, setToken] = useState<string | null>(localStorage.getItem('token'));
|
const [token, setToken] = useState<string | null>(getValidToken);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const savedToken = localStorage.getItem('token');
|
const onAuthLogout = () => setToken(null);
|
||||||
if (savedToken) {
|
window.addEventListener('auth:logout', onAuthLogout);
|
||||||
setToken(savedToken);
|
return () => window.removeEventListener('auth:logout', onAuthLogout);
|
||||||
}
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleLogin = (newToken: string) => {
|
const handleLogin = (newToken: string) => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
import './Dashboard.css';
|
import './Dashboard.css';
|
||||||
import { Shield, Users, Activity, Clock } from 'lucide-react';
|
import { Shield, Users, Activity, Clock } from 'lucide-react';
|
||||||
|
|
||||||
@@ -29,8 +29,15 @@ const Dashboard: React.FC<DashboardProps> = ({ searchQuery }) => {
|
|||||||
const [stats, setStats] = useState<Stats | null>(null);
|
const [stats, setStats] = useState<Stats | null>(null);
|
||||||
const [logs, setLogs] = useState<LogEntry[]>([]);
|
const [logs, setLogs] = useState<LogEntry[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const eventSourceRef = useRef<EventSource | null>(null);
|
||||||
|
const reconnectTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const connect = () => {
|
||||||
|
if (eventSourceRef.current) {
|
||||||
|
eventSourceRef.current.close();
|
||||||
|
}
|
||||||
|
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
const baseUrl = import.meta.env.VITE_API_URL || 'http://localhost:8000/api/v1';
|
const baseUrl = import.meta.env.VITE_API_URL || 'http://localhost:8000/api/v1';
|
||||||
let url = `${baseUrl}/stream?token=${token}`;
|
let url = `${baseUrl}/stream?token=${token}`;
|
||||||
@@ -38,9 +45,10 @@ const Dashboard: React.FC<DashboardProps> = ({ searchQuery }) => {
|
|||||||
url += `&search=${encodeURIComponent(searchQuery)}`;
|
url += `&search=${encodeURIComponent(searchQuery)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventSource = new EventSource(url);
|
const es = new EventSource(url);
|
||||||
|
eventSourceRef.current = es;
|
||||||
|
|
||||||
eventSource.onmessage = (event) => {
|
es.onmessage = (event) => {
|
||||||
try {
|
try {
|
||||||
const payload = JSON.parse(event.data);
|
const payload = JSON.parse(event.data);
|
||||||
if (payload.type === 'logs') {
|
if (payload.type === 'logs') {
|
||||||
@@ -48,18 +56,25 @@ const Dashboard: React.FC<DashboardProps> = ({ searchQuery }) => {
|
|||||||
} else if (payload.type === 'stats') {
|
} else if (payload.type === 'stats') {
|
||||||
setStats(payload.data);
|
setStats(payload.data);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
window.dispatchEvent(new CustomEvent('decnet:stats', { detail: payload.data }));
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to parse SSE payload', err);
|
console.error('Failed to parse SSE payload', err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
eventSource.onerror = (err) => {
|
es.onerror = () => {
|
||||||
console.error('SSE connection error, attempting to reconnect...', err);
|
es.close();
|
||||||
|
eventSourceRef.current = null;
|
||||||
|
reconnectTimerRef.current = setTimeout(connect, 3000);
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
connect();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
eventSource.close();
|
if (reconnectTimerRef.current) clearTimeout(reconnectTimerRef.current);
|
||||||
|
if (eventSourceRef.current) eventSourceRef.current.close();
|
||||||
};
|
};
|
||||||
}, [searchQuery]);
|
}, [searchQuery]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
import { Menu, X, Search, Activity, LayoutDashboard, Terminal, Settings, LogOut, Server, Archive } from 'lucide-react';
|
import { Menu, X, Search, Activity, LayoutDashboard, Terminal, Settings, LogOut, Server, Archive } from 'lucide-react';
|
||||||
import api from '../utils/api';
|
|
||||||
import './Layout.css';
|
import './Layout.css';
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
@@ -21,17 +20,12 @@ const Layout: React.FC<LayoutProps> = ({ children, onLogout, onSearch }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchStatus = async () => {
|
const onStats = (e: Event) => {
|
||||||
try {
|
const stats = (e as CustomEvent).detail;
|
||||||
const res = await api.get('/stats');
|
setSystemActive(stats.deployed_deckies > 0);
|
||||||
setSystemActive(res.data.deployed_deckies > 0);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to fetch system status', err);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
fetchStatus();
|
window.addEventListener('decnet:stats', onStats);
|
||||||
const interval = setInterval(fetchStatus, 10000);
|
return () => window.removeEventListener('decnet:stats', onStats);
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -12,4 +12,15 @@ api.interceptors.request.use((config) => {
|
|||||||
return config;
|
return config;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
api.interceptors.response.use(
|
||||||
|
(response) => response,
|
||||||
|
(error) => {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
window.dispatchEvent(new Event('auth:logout'));
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ Requires DECNET_DEVELOPER=true (set in tests/conftest.py) to expose /openapi.jso
|
|||||||
"""
|
"""
|
||||||
import pytest
|
import pytest
|
||||||
import schemathesis as st
|
import schemathesis as st
|
||||||
from hypothesis import settings, Verbosity
|
from hypothesis import settings, Verbosity, HealthCheck
|
||||||
from decnet.web.auth import create_access_token
|
from decnet.web.auth import create_access_token
|
||||||
|
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -102,6 +102,6 @@ schema = st.openapi.from_url(f"{LIVE_SERVER_URL}/openapi.json")
|
|||||||
|
|
||||||
@pytest.mark.fuzz
|
@pytest.mark.fuzz
|
||||||
@st.pytest.parametrize(api=schema)
|
@st.pytest.parametrize(api=schema)
|
||||||
@settings(max_examples=3000, deadline=None, verbosity=Verbosity.debug)
|
@settings(max_examples=3000, deadline=None, verbosity=Verbosity.debug, suppress_health_check=[HealthCheck.filter_too_much])
|
||||||
def test_schema_compliance(case):
|
def test_schema_compliance(case):
|
||||||
case.call_and_validate()
|
case.call_and_validate()
|
||||||
|
|||||||
Reference in New Issue
Block a user