Add functionality to manage and test router connections

Implement dialogs and forms for adding/editing routers, along with backend endpoints for updating router details and testing connectivity.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 7a657272-55ba-4a79-9a2e-f1ed9bc7a528
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Event-Id: 72dce443-ff50-4028-b2d4-a6b504b9b018
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/449cf7c4-c97a-45ae-8234-e5c5b8d6a84f/7a657272-55ba-4a79-9a2e-f1ed9bc7a528/L6QSDnx
This commit is contained in:
marco370 2025-11-25 11:01:18 +00:00
parent 7c204c62b2
commit 8aabed0272
3 changed files with 513 additions and 6 deletions

View File

@ -26,6 +26,10 @@ externalPort = 3003
localPort = 43803
externalPort = 3000
[[ports]]
localPort = 46817
externalPort = 3001
[env]
PORT = "5000"

View File

@ -1,19 +1,109 @@
import { useState } from "react";
import { useQuery, useMutation } from "@tanstack/react-query";
import { queryClient, apiRequest } from "@/lib/queryClient";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Server, Plus, Trash2 } from "lucide-react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogFooter,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { Server, Plus, Trash2, Edit, Wifi, WifiOff } from "lucide-react";
import { format } from "date-fns";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { insertRouterSchema, type InsertRouter } from "@shared/schema";
import type { Router } from "@shared/schema";
import { useToast } from "@/hooks/use-toast";
export default function Routers() {
const { toast } = useToast();
const [addDialogOpen, setAddDialogOpen] = useState(false);
const [editDialogOpen, setEditDialogOpen] = useState(false);
const [editingRouter, setEditingRouter] = useState<Router | null>(null);
const [testingRouterId, setTestingRouterId] = useState<string | null>(null);
const { data: routers, isLoading } = useQuery<Router[]>({
queryKey: ["/api/routers"],
});
const addForm = useForm<InsertRouter>({
resolver: zodResolver(insertRouterSchema),
defaultValues: {
name: "",
ipAddress: "",
apiPort: 8728,
username: "",
password: "",
enabled: true,
},
});
const editForm = useForm<InsertRouter>({
resolver: zodResolver(insertRouterSchema),
});
const addMutation = useMutation({
mutationFn: async (data: InsertRouter) => {
return await apiRequest("POST", "/api/routers", data);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/routers"] });
toast({
title: "Router aggiunto",
description: "Il router è stato configurato con successo",
});
setAddDialogOpen(false);
addForm.reset();
},
onError: (error: any) => {
toast({
title: "Errore",
description: error.message || "Impossibile aggiungere il router",
variant: "destructive",
});
},
});
const updateMutation = useMutation({
mutationFn: async ({ id, data }: { id: string; data: InsertRouter }) => {
return await apiRequest("PUT", `/api/routers/${id}`, data);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/routers"] });
toast({
title: "Router aggiornato",
description: "Le modifiche sono state salvate con successo",
});
setEditDialogOpen(false);
setEditingRouter(null);
editForm.reset();
},
onError: (error: any) => {
toast({
title: "Errore",
description: error.message || "Impossibile aggiornare il router",
variant: "destructive",
});
},
});
const deleteMutation = useMutation({
mutationFn: async (id: string) => {
await apiRequest("DELETE", `/api/routers/${id}`);
@ -34,6 +124,56 @@ export default function Routers() {
},
});
const testConnectionMutation = useMutation({
mutationFn: async (id: string) => {
const response = await apiRequest("POST", `/api/routers/${id}/test`);
return response;
},
onSuccess: (data: any) => {
toast({
title: "Connessione riuscita",
description: data.message || "Il router è raggiungibile e le credenziali sono corrette",
});
setTestingRouterId(null);
},
onError: (error: any) => {
toast({
title: "Connessione fallita",
description: error.message || "Impossibile connettersi al router. Verifica IP, porta e credenziali.",
variant: "destructive",
});
setTestingRouterId(null);
},
});
const handleAddSubmit = (data: InsertRouter) => {
addMutation.mutate(data);
};
const handleEditSubmit = (data: InsertRouter) => {
if (editingRouter) {
updateMutation.mutate({ id: editingRouter.id, data });
}
};
const handleEdit = (router: Router) => {
setEditingRouter(router);
editForm.reset({
name: router.name,
ipAddress: router.ipAddress,
apiPort: router.apiPort,
username: router.username,
password: router.password,
enabled: router.enabled,
});
setEditDialogOpen(true);
};
const handleTestConnection = (id: string) => {
setTestingRouterId(id);
testConnectionMutation.mutate(id);
};
return (
<div className="flex flex-col gap-6 p-6" data-testid="page-routers">
<div className="flex items-center justify-between">
@ -43,10 +183,152 @@ export default function Routers() {
Gestisci i router connessi al sistema IDS
</p>
</div>
<Button data-testid="button-add-router">
<Plus className="h-4 w-4 mr-2" />
Aggiungi Router
</Button>
<Dialog open={addDialogOpen} onOpenChange={setAddDialogOpen}>
<DialogTrigger asChild>
<Button data-testid="button-add-router">
<Plus className="h-4 w-4 mr-2" />
Aggiungi Router
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[500px]" data-testid="dialog-add-router">
<DialogHeader>
<DialogTitle>Aggiungi Router MikroTik</DialogTitle>
<DialogDescription>
Configura un nuovo router MikroTik per il sistema IDS. Assicurati che l'API REST sia abilitata.
</DialogDescription>
</DialogHeader>
<Form {...addForm}>
<form onSubmit={addForm.handleSubmit(handleAddSubmit)} className="space-y-4">
<FormField
control={addForm.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Nome Router</FormLabel>
<FormControl>
<Input placeholder="es. MikroTik Ufficio" {...field} data-testid="input-name" />
</FormControl>
<FormDescription>
Nome descrittivo per identificare il router
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={addForm.control}
name="ipAddress"
render={({ field }) => (
<FormItem>
<FormLabel>Indirizzo IP</FormLabel>
<FormControl>
<Input placeholder="es. 192.168.1.1" {...field} data-testid="input-ip" />
</FormControl>
<FormDescription>
Indirizzo IP o hostname del router
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={addForm.control}
name="apiPort"
render={({ field }) => (
<FormItem>
<FormLabel>Porta API</FormLabel>
<FormControl>
<Input
type="number"
placeholder="8728"
{...field}
onChange={(e) => field.onChange(parseInt(e.target.value))}
data-testid="input-port"
/>
</FormControl>
<FormDescription>
Porta API MikroTik (default: 8728 per API, 8729 per API-SSL)
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={addForm.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="admin" {...field} data-testid="input-username" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={addForm.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" placeholder="••••••••" {...field} data-testid="input-password" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={addForm.control}
name="enabled"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
<div className="space-y-0.5">
<FormLabel>Abilitato</FormLabel>
<FormDescription>
Attiva il router per il blocco automatico degli IP
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
data-testid="switch-enabled"
/>
</FormControl>
</FormItem>
)}
/>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => setAddDialogOpen(false)}
data-testid="button-cancel"
>
Annulla
</Button>
<Button
type="submit"
disabled={addMutation.isPending}
data-testid="button-submit"
>
{addMutation.isPending ? "Salvataggio..." : "Salva Router"}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
</div>
<Card data-testid="card-routers">
@ -114,9 +396,24 @@ export default function Routers() {
variant="outline"
size="sm"
className="flex-1"
onClick={() => handleTestConnection(router.id)}
disabled={testingRouterId === router.id}
data-testid={`button-test-${router.id}`}
>
Test Connessione
{testingRouterId === router.id ? (
<WifiOff className="h-4 w-4 mr-2 animate-pulse" />
) : (
<Wifi className="h-4 w-4 mr-2" />
)}
{testingRouterId === router.id ? "Test..." : "Test"}
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleEdit(router)}
data-testid={`button-edit-${router.id}`}
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="outline"
@ -140,6 +437,137 @@ export default function Routers() {
)}
</CardContent>
</Card>
<Dialog open={editDialogOpen} onOpenChange={setEditDialogOpen}>
<DialogContent className="sm:max-w-[500px]" data-testid="dialog-edit-router">
<DialogHeader>
<DialogTitle>Modifica Router</DialogTitle>
<DialogDescription>
Modifica le impostazioni del router {editingRouter?.name}
</DialogDescription>
</DialogHeader>
<Form {...editForm}>
<form onSubmit={editForm.handleSubmit(handleEditSubmit)} className="space-y-4">
<FormField
control={editForm.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Nome Router</FormLabel>
<FormControl>
<Input placeholder="es. MikroTik Ufficio" {...field} data-testid="input-edit-name" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="ipAddress"
render={({ field }) => (
<FormItem>
<FormLabel>Indirizzo IP</FormLabel>
<FormControl>
<Input placeholder="es. 192.168.1.1" {...field} data-testid="input-edit-ip" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="apiPort"
render={({ field }) => (
<FormItem>
<FormLabel>Porta API</FormLabel>
<FormControl>
<Input
type="number"
placeholder="8728"
{...field}
onChange={(e) => field.onChange(parseInt(e.target.value))}
data-testid="input-edit-port"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="admin" {...field} data-testid="input-edit-username" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" placeholder="••••••••" {...field} data-testid="input-edit-password" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="enabled"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
<div className="space-y-0.5">
<FormLabel>Abilitato</FormLabel>
<FormDescription>
Attiva il router per il blocco automatico degli IP
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
data-testid="switch-edit-enabled"
/>
</FormControl>
</FormItem>
)}
/>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => setEditDialogOpen(false)}
data-testid="button-edit-cancel"
>
Annulla
</Button>
<Button
type="submit"
disabled={updateMutation.isPending}
data-testid="button-edit-submit"
>
{updateMutation.isPending ? "Salvataggio..." : "Salva Modifiche"}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
</div>
);
}

View File

@ -27,6 +27,20 @@ export async function registerRoutes(app: Express): Promise<Server> {
}
});
app.put("/api/routers/:id", async (req, res) => {
try {
const validatedData = insertRouterSchema.parse(req.body);
const router = await storage.updateRouter(req.params.id, validatedData);
if (!router) {
return res.status(404).json({ error: "Router not found" });
}
res.json(router);
} catch (error) {
console.error('[Router UPDATE] Error:', error);
res.status(400).json({ error: "Invalid router data" });
}
});
app.delete("/api/routers/:id", async (req, res) => {
try {
const success = await storage.deleteRouter(req.params.id);
@ -39,6 +53,67 @@ export async function registerRoutes(app: Express): Promise<Server> {
}
});
app.post("/api/routers/:id/test", async (req, res) => {
try {
const router = await storage.getRouterById(req.params.id);
if (!router) {
return res.status(404).json({ error: "Router not found" });
}
// Test connessione TCP/HTTP al router
const testUrl = `http://${router.ipAddress}:${router.apiPort}`;
try {
// Timeout di 5 secondi per il test
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
const response = await fetch(testUrl, {
method: 'GET',
signal: controller.signal,
headers: {
'Authorization': 'Basic ' + Buffer.from(`${router.username}:${router.password}`).toString('base64')
}
});
clearTimeout(timeoutId);
// Aggiorna lastSync
await storage.updateRouter(router.id, { lastSync: new Date() });
res.json({
success: true,
message: `Router ${router.name} raggiungibile (HTTP ${response.status})`,
status: response.status,
statusText: response.statusText
});
} catch (fetchError: any) {
console.error('[Router TEST] Connection failed:', fetchError.message);
// Differenzia gli errori
if (fetchError.name === 'AbortError') {
res.status(408).json({
error: "Timeout: Il router non risponde entro 5 secondi",
message: `Impossibile connettersi a ${router.ipAddress}:${router.apiPort}. Verifica che il router sia acceso e raggiungibile.`
});
} else if (fetchError.code === 'ECONNREFUSED') {
res.status(503).json({
error: "Connessione rifiutata",
message: `Il router a ${router.ipAddress}:${router.apiPort} rifiuta la connessione. Verifica che l'API REST sia abilitata.`
});
} else {
res.status(503).json({
error: "Errore di connessione",
message: `${fetchError.message}. Verifica IP, porta e firewall.`
});
}
}
} catch (error) {
console.error('[Router TEST] Error:', error);
res.status(500).json({ error: "Failed to test router connection" });
}
});
// Network Logs
app.get("/api/logs", async (req, res) => {
try {