refactor: simplify API routes and add sniffer status functionality
Some checks failed
Build and Deploy MITM Webserver / build (push) Failing after 5s
Some checks failed
Build and Deploy MITM Webserver / build (push) Failing after 5s
- Updated API routes to remove redundant prefixes. - Introduced sniffer status fetching in the backend API. - Added Sniffing page and integrated it into the routing. - Created a new hook for backend API interactions. - Implemented sniffer status type definitions.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// src/components/Sidebar.tsx
|
||||
import { ApartmentOutlined, HomeOutlined, InfoCircleOutlined, SettingOutlined } from '@ant-design/icons';
|
||||
import { ApartmentOutlined, ApiOutlined, HomeOutlined, InfoCircleOutlined, SettingOutlined } from '@ant-design/icons';
|
||||
import { Layout, Menu } from 'antd';
|
||||
import React from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
@@ -11,6 +11,7 @@ const { Sider } = Layout;
|
||||
const menuItems = [
|
||||
{ key: PATHS.HOME, icon: <HomeOutlined style={{ fontSize: '18px' }} />, label: 'Home' },
|
||||
{ key: PATHS.NETWORK, icon: <ApartmentOutlined style={{ fontSize: '18px' }} />, label: 'Network' },
|
||||
{ key: PATHS.SNIFFING, icon: <ApiOutlined style={{ fontSize: '18px' }} />, label: 'Sniffing' },
|
||||
{ key: '/about', icon: <InfoCircleOutlined style={{ fontSize: '18px' }} />, label: 'About' },
|
||||
{ key: '/settings', icon: <SettingOutlined style={{ fontSize: '18px' }} />, label: 'Settings' },
|
||||
];
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
// src/apiClient.ts
|
||||
import axios from "axios";
|
||||
import {
|
||||
BridgeCreateRequest,
|
||||
BridgeInfo,
|
||||
BridgeRemoveRequest,
|
||||
FullState,
|
||||
InterfaceInfo,
|
||||
RouteInfo,
|
||||
BridgeCreateRequest,
|
||||
BridgeInfo,
|
||||
BridgeRemoveRequest,
|
||||
FullState,
|
||||
InterfaceInfo,
|
||||
RouteInfo,
|
||||
} from "../types/network";
|
||||
import { SnifferStatusResponse } from "../types/sniffer";
|
||||
|
||||
const BASE = "http://mitm.lan/api";
|
||||
|
||||
@@ -17,7 +18,7 @@ export const api = axios.create({
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
// 🔹 Normalize FastAPI errors here
|
||||
// Normalize FastAPI errors here
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
@@ -59,6 +60,12 @@ export const fetchFullState = async (): Promise<FullState> => {
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const fetchSnifferStatus = async (): Promise<SnifferStatusResponse> => {
|
||||
const res = await api.get<SnifferStatusResponse>("/sniffer/status");
|
||||
return res.data;
|
||||
};
|
||||
|
||||
|
||||
// Mutations
|
||||
export const createBridge = async (req: BridgeCreateRequest) => {
|
||||
const res = await api.post("network/bridge/create", req);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
import App from './App'; // your layout component (has <Outlet />)
|
||||
import Home from './pages/Home';
|
||||
import Network from './pages/Network';
|
||||
import Sniffing from './pages/Sniffing';
|
||||
import { PATHS } from './routes';
|
||||
|
||||
export default function AppRouter() {
|
||||
@@ -16,6 +17,7 @@ export default function AppRouter() {
|
||||
{/* Child routes - these render inside App's <Outlet /> */}
|
||||
<Route path={PATHS.HOME.slice(1)} element={<Home />} />
|
||||
<Route path={PATHS.NETWORK.slice(1)} element={<Network />} />
|
||||
<Route path={PATHS.SNIFFING.slice(1)} element={<Sniffing />} />
|
||||
|
||||
{/* Fallback (renders inside layout too) */}
|
||||
<Route path="*" element={<NotFound />} />
|
||||
|
||||
@@ -3,13 +3,14 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useCallback, useState } from "react";
|
||||
import * as api from "../api/apiClient";
|
||||
import type {
|
||||
BridgeCreateRequest,
|
||||
BridgeInfo,
|
||||
BridgeRemoveRequest,
|
||||
FullState,
|
||||
InterfaceInfo,
|
||||
RouteInfo,
|
||||
BridgeCreateRequest,
|
||||
BridgeInfo,
|
||||
BridgeRemoveRequest,
|
||||
FullState,
|
||||
InterfaceInfo,
|
||||
RouteInfo,
|
||||
} from "../types/network";
|
||||
import { SnifferStatusResponse } from "../types/sniffer";
|
||||
|
||||
/**
|
||||
* useNetwork
|
||||
@@ -22,7 +23,7 @@ import type {
|
||||
*
|
||||
* This gives "no initial auto-fetch" but "once fetched, auto-updates".
|
||||
*/
|
||||
export function useNetwork() {
|
||||
export function useBackendAPI() {
|
||||
const qc = useQueryClient();
|
||||
|
||||
// per-query enabled flags (start false => no automatic fetch)
|
||||
@@ -74,6 +75,13 @@ export function useNetwork() {
|
||||
...commonOptions,
|
||||
});
|
||||
|
||||
const snifferStatusQuery = useQuery<SnifferStatusResponse>({
|
||||
queryKey: ["sniffer-status"],
|
||||
queryFn: api.fetchSnifferStatus,
|
||||
enabled: fullStateEnabled,
|
||||
...commonOptions,
|
||||
});
|
||||
|
||||
// Imperative fetch helpers that also enable auto-refetch behavior
|
||||
const fetchInterfaces = useCallback(async () => {
|
||||
const res = await qc.fetchQuery<InterfaceInfo[]>({
|
||||
@@ -125,6 +133,16 @@ export function useNetwork() {
|
||||
return res;
|
||||
}, [qc]);
|
||||
|
||||
const fetchSnifferStatus = useCallback(async () => {
|
||||
const res = await qc.fetchQuery<SnifferStatusResponse>({
|
||||
queryKey: ["sniffer-status"],
|
||||
queryFn: api.fetchSnifferStatus,
|
||||
staleTime: 1000 * 5
|
||||
});
|
||||
setFullStateEnabled(true);
|
||||
return res;
|
||||
}, [qc]);
|
||||
|
||||
// Local loading state for simple UI feedback
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [isRemoving, setIsRemoving] = useState(false);
|
||||
@@ -154,6 +172,7 @@ export function useNetwork() {
|
||||
await qc.invalidateQueries({ queryKey: ["bridges"] });
|
||||
await qc.invalidateQueries({ queryKey: ["full-state"] });
|
||||
await qc.invalidateQueries({ queryKey: ["interfaces"] });
|
||||
await qc.invalidateQueries({ queryKey: ["sniffer-status"] });
|
||||
} catch (err) {
|
||||
const message =
|
||||
err instanceof Error ? err.message : typeof err === "string" ? err : "Remove bridge failed";
|
||||
@@ -184,6 +203,10 @@ export function useNetwork() {
|
||||
return qc.invalidateQueries({ queryKey: ["full-state"] });
|
||||
}
|
||||
|
||||
function refreshSnifferStatus() {
|
||||
return qc.invalidateQueries({ queryKey: ["sniffer-status"] });
|
||||
}
|
||||
|
||||
// Convenience: refresh all queries
|
||||
function refreshAll() {
|
||||
refreshInterfaces();
|
||||
@@ -191,6 +214,7 @@ export function useNetwork() {
|
||||
refreshRoutes();
|
||||
refreshBridges();
|
||||
refreshFullState();
|
||||
refreshSnifferStatus();
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -200,6 +224,7 @@ export function useNetwork() {
|
||||
routesQuery,
|
||||
bridgesQuery,
|
||||
fullStateQuery,
|
||||
snifferStatusQuery,
|
||||
|
||||
// manual fetchers (fetch+enable auto-updates)
|
||||
fetchInterfaces,
|
||||
@@ -207,6 +232,7 @@ export function useNetwork() {
|
||||
fetchRoutes,
|
||||
fetchBridges,
|
||||
fetchFullState,
|
||||
fetchSnifferStatus,
|
||||
|
||||
// simple mutation functions (imperative)
|
||||
createBridge,
|
||||
@@ -222,6 +248,9 @@ export function useNetwork() {
|
||||
refreshRoutes,
|
||||
refreshBridges,
|
||||
refreshFullState,
|
||||
refreshSnifferStatus,
|
||||
|
||||
// refresh all
|
||||
refreshAll,
|
||||
};
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useEffect } from 'react';
|
||||
import { fetchFullState } from '../api/apiClient';
|
||||
import { useNetwork } from '../hooks/useNetwork';
|
||||
import { useBackendAPI } from '../hooks/useBackendAPI';
|
||||
|
||||
export default function Home() {
|
||||
const { fullStateQuery } = useNetwork();
|
||||
const { fullStateQuery } = useBackendAPI();
|
||||
const { data, isLoading, isError, error } = fullStateQuery;
|
||||
|
||||
if (isLoading) return <div>Loading full state…</div>;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { DeleteOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||
import { Button, Col, Form, Input, message, Modal, Popconfirm, Row, Select, Space, Table, Tag, Typography } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useNetwork } from '../hooks/useNetwork';
|
||||
import { useBackendAPI } from '../hooks/useBackendAPI';
|
||||
import type { BridgeInfo, InterfaceInfo } from '../types/network';
|
||||
|
||||
const { Title, Paragraph } = Typography;
|
||||
@@ -20,7 +20,7 @@ export default function Network() {
|
||||
refreshInterfaces,
|
||||
isCreating,
|
||||
isRemoving,
|
||||
} = useNetwork();
|
||||
} = useBackendAPI();
|
||||
|
||||
const [bridgeModalVisible, setBridgeModalVisible] = useState(false);
|
||||
const [bridgeForm] = Form.useForm();
|
||||
|
||||
32
frontend/src/pages/Sniffing.tsx
Normal file
32
frontend/src/pages/Sniffing.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useBackendAPI } from '../hooks/useBackendAPI';
|
||||
import { fetchSnifferStatus } from '../api/apiClient';
|
||||
|
||||
export default function Sniffing() {
|
||||
const {
|
||||
interfacesQuery,
|
||||
bridgesQuery,
|
||||
createBridge,
|
||||
removeBridge,
|
||||
fetchBridges,
|
||||
fetchInterfaces,
|
||||
refreshBridges,
|
||||
refreshInterfaces,
|
||||
isCreating,
|
||||
isRemoving,
|
||||
} = useBackendAPI();
|
||||
|
||||
// fetch on mount (explicit, since queries are disabled by default)
|
||||
useEffect(() => {
|
||||
fetchInterfaces().catch(() => {});
|
||||
fetchSnifferStatus().catch(() => {});
|
||||
}, []);
|
||||
|
||||
const bridges = bridgesQuery.data ?? [];
|
||||
const snifferStatus =
|
||||
return (
|
||||
<div className="sniffing-body">
|
||||
<h1>Sniffing</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,4 +2,5 @@ export const PATHS = {
|
||||
ROOT: '/',
|
||||
HOME: '/home',
|
||||
NETWORK: '/network',
|
||||
SNIFFING: '/sniffing',
|
||||
};
|
||||
21
frontend/src/types/sniffer.ts
Normal file
21
frontend/src/types/sniffer.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Status of an individual interface monitored by the AF_PACKET sniffer.
|
||||
*/
|
||||
export interface InterfaceSnifferStatus {
|
||||
/** Whether the sniffer thread is active */
|
||||
running: boolean;
|
||||
|
||||
/** Whether the interface exists in /sys/class/net */
|
||||
exists: boolean;
|
||||
|
||||
/** Whether the interface is operationally UP */
|
||||
up: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Response model for the sniffer status endpoint.
|
||||
* Maps interface names to their sniffer status.
|
||||
*/
|
||||
export interface SnifferStatusResponse {
|
||||
interfaces: Record<string, InterfaceSnifferStatus>;
|
||||
}
|
||||
Reference in New Issue
Block a user