From 7b514f470f09c652eee53e8b853f61f09f1016d3 Mon Sep 17 00:00:00 2001 From: marco370 <48531002-marco370@users.noreply.replit.com> Date: Fri, 21 Nov 2025 09:08:38 +0000 Subject: [PATCH] Implement form validation and improve error handling for ML features Refactor Training and Whitelist pages to use react-hook-form and Zod for validation, and enhance ML backend API routes with timeouts, input validation, and better error reporting. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 7a657272-55ba-4a79-9a2e-f1ed9bc7a528 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: 95d9d0e3-3da7-43ff-b8d3-d9d5d8fd6f6f Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/449cf7c4-c97a-45ae-8234-e5c5b8d6a84f/7a657272-55ba-4a79-9a2e-f1ed9bc7a528/Aqah4U9 --- .replit | 4 +- client/src/pages/Training.tsx | 302 +++++++++++++++++++-------------- client/src/pages/Whitelist.tsx | 161 +++++++++++------- server/routes.ts | 97 +++++++++-- 4 files changed, 353 insertions(+), 211 deletions(-) diff --git a/.replit b/.replit index 26a22e3..30bea15 100644 --- a/.replit +++ b/.replit @@ -15,8 +15,8 @@ localPort = 5000 externalPort = 80 [[ports]] -localPort = 43079 -externalPort = 3001 +localPort = 41303 +externalPort = 3002 [[ports]] localPort = 43803 diff --git a/client/src/pages/Training.tsx b/client/src/pages/Training.tsx index e48bf57..cefd634 100644 --- a/client/src/pages/Training.tsx +++ b/client/src/pages/Training.tsx @@ -8,8 +8,9 @@ import { format } from "date-fns"; import type { TrainingHistory } from "@shared/schema"; import { useToast } from "@/hooks/use-toast"; import { useState } from "react"; -import { Label } from "@/components/ui/label"; -import { Input } from "@/components/ui/input"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; import { Dialog, DialogContent, @@ -19,6 +20,17 @@ import { DialogTrigger, DialogFooter, } from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, + FormDescription, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Checkbox } from "@/components/ui/checkbox"; interface MLStatsResponse { logs?: { total: number; last_hour: number }; @@ -27,16 +39,40 @@ interface MLStatsResponse { latest_training?: any; } +const trainFormSchema = z.object({ + max_records: z.coerce.number().min(1, "Minimo 1 record").max(1000000, "Massimo 1M record"), + hours_back: z.coerce.number().min(1, "Minimo 1 ora").max(720, "Massimo 720 ore (30 giorni)"), +}); + +const detectFormSchema = z.object({ + max_records: z.coerce.number().min(1, "Minimo 1 record").max(1000000, "Massimo 1M record"), + hours_back: z.coerce.number().min(1, "Minimo 1 ora").max(720, "Massimo 720 ore"), + risk_threshold: z.coerce.number().min(0, "Minimo 0").max(100, "Massimo 100"), + auto_block: z.boolean().default(true), +}); + export default function TrainingPage() { const { toast } = useToast(); const [isTrainDialogOpen, setIsTrainDialogOpen] = useState(false); const [isDetectDialogOpen, setIsDetectDialogOpen] = useState(false); - const [trainRecords, setTrainRecords] = useState("100000"); - const [trainHours, setTrainHours] = useState("24"); - const [detectRecords, setDetectRecords] = useState("50000"); - const [detectHours, setDetectHours] = useState("1"); - const [detectThreshold, setDetectThreshold] = useState("75"); - const [detectAutoBlock, setDetectAutoBlock] = useState(true); + + const trainForm = useForm>({ + resolver: zodResolver(trainFormSchema), + defaultValues: { + max_records: 100000, + hours_back: 24, + }, + }); + + const detectForm = useForm>({ + resolver: zodResolver(detectFormSchema), + defaultValues: { + max_records: 50000, + hours_back: 1, + risk_threshold: 75, + auto_block: true, + }, + }); const { data: history, isLoading } = useQuery({ queryKey: ["/api/training-history"], @@ -49,7 +85,7 @@ export default function TrainingPage() { }); const trainMutation = useMutation({ - mutationFn: async (params: { max_records: number; hours_back: number }) => { + mutationFn: async (params: z.infer) => { return await apiRequest("POST", "/api/ml/train", params); }, onSuccess: () => { @@ -60,18 +96,19 @@ export default function TrainingPage() { description: "Il modello ML è in addestramento. Controlla lo storico tra qualche minuto.", }); setIsTrainDialogOpen(false); + trainForm.reset(); }, - onError: () => { + onError: (error: any) => { toast({ title: "Errore", - description: "Impossibile avviare il training", + description: error.message || "Impossibile avviare il training", variant: "destructive", }); }, }); const detectMutation = useMutation({ - mutationFn: async (params: { max_records: number; hours_back: number; risk_threshold: number; auto_block: boolean }) => { + mutationFn: async (params: z.infer) => { return await apiRequest("POST", "/api/ml/detect", params); }, onSuccess: () => { @@ -82,52 +119,23 @@ export default function TrainingPage() { description: "Analisi anomalie in corso. Controlla i rilevamenti tra qualche secondo.", }); setIsDetectDialogOpen(false); + detectForm.reset(); }, - onError: () => { + onError: (error: any) => { toast({ title: "Errore", - description: "Impossibile avviare la detection", + description: error.message || "Impossibile avviare la detection", variant: "destructive", }); }, }); - const handleTrain = () => { - const records = parseInt(trainRecords); - const hours = parseInt(trainHours); - - if (isNaN(records) || records <= 0) { - toast({ - title: "Errore", - description: "Inserisci un numero valido di record", - variant: "destructive", - }); - return; - } - - trainMutation.mutate({ max_records: records, hours_back: hours }); + const onTrainSubmit = (data: z.infer) => { + trainMutation.mutate(data); }; - const handleDetect = () => { - const records = parseInt(detectRecords); - const hours = parseInt(detectHours); - const threshold = parseInt(detectThreshold); - - if (isNaN(records) || records <= 0 || isNaN(threshold) || threshold < 0 || threshold > 100) { - toast({ - title: "Errore", - description: "Inserisci valori validi", - variant: "destructive", - }); - return; - } - - detectMutation.mutate({ - max_records: records, - hours_back: hours, - risk_threshold: threshold, - auto_block: detectAutoBlock, - }); + const onDetectSubmit = (data: z.infer) => { + detectMutation.mutate(data); }; return ( @@ -213,38 +221,51 @@ export default function TrainingPage() { Configura i parametri per l'addestramento del modello -
-
- - setTrainRecords(e.target.value)} - data-testid="input-train-records" +
+ + ( + + Numero Record + + + + Consigliato: 100000 + + + )} /> -

Consigliato: 100000

-
-
- - setTrainHours(e.target.value)} - data-testid="input-train-hours" + ( + + Ore Precedenti + + + + Consigliato: 24 + + + )} /> -

Consigliato: 24

-
-
- - - - + + + + + + @@ -275,62 +296,83 @@ export default function TrainingPage() { Configura i parametri per il rilevamento anomalie -
-
- - setDetectRecords(e.target.value)} - data-testid="input-detect-records" +
+ + ( + + Numero Record + + + + Consigliato: 50000 + + + )} /> -

Consigliato: 50000

-
-
- - setDetectHours(e.target.value)} - data-testid="input-detect-hours" + ( + + Ore Precedenti + + + + Consigliato: 1 + + + )} /> -

Consigliato: 1

-
-
- - setDetectThreshold(e.target.value)} - data-testid="input-detect-threshold" + ( + + Soglia Rischio (%) + + + + Consigliato: 75 + + + )} /> -

Consigliato: 75

-
-
- setDetectAutoBlock(e.target.checked)} - className="rounded border-gray-300" - data-testid="checkbox-auto-block" + ( + + + + +
+ Blocco automatico IP critici (≥80) +
+
+ )} /> - -
-
- - - - + + + + + + diff --git a/client/src/pages/Whitelist.tsx b/client/src/pages/Whitelist.tsx index ab5a2aa..b28f1a9 100644 --- a/client/src/pages/Whitelist.tsx +++ b/client/src/pages/Whitelist.tsx @@ -2,13 +2,14 @@ import { useQuery, useMutation } from "@tanstack/react-query"; import { queryClient, apiRequest } from "@/lib/queryClient"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Textarea } from "@/components/ui/textarea"; import { Shield, Plus, Trash2, CheckCircle2, XCircle } from "lucide-react"; import { format } from "date-fns"; import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; import type { Whitelist } from "@shared/schema"; +import { insertWhitelistSchema } from "@shared/schema"; import { useToast } from "@/hooks/use-toast"; import { Dialog, @@ -19,20 +20,47 @@ import { DialogTrigger, DialogFooter, } from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; + +const whitelistFormSchema = insertWhitelistSchema.extend({ + ipAddress: z.string() + .min(7, "Inserisci un IP valido") + .regex(/^(\d{1,3}\.){3}\d{1,3}$/, "Formato IP non valido") + .refine((ip) => { + const parts = ip.split('.').map(Number); + return parts.every(part => part >= 0 && part <= 255); + }, "Ogni ottetto deve essere tra 0 e 255"), +}); export default function WhitelistPage() { const { toast } = useToast(); const [isAddDialogOpen, setIsAddDialogOpen] = useState(false); - const [newIp, setNewIp] = useState(""); - const [newComment, setNewComment] = useState(""); - const [newReason, setNewReason] = useState(""); + + const form = useForm>({ + resolver: zodResolver(whitelistFormSchema), + defaultValues: { + ipAddress: "", + comment: "", + reason: "", + active: true, + }, + }); const { data: whitelist, isLoading } = useQuery({ queryKey: ["/api/whitelist"], }); const addMutation = useMutation({ - mutationFn: async (data: { ipAddress: string; comment?: string; reason?: string }) => { + mutationFn: async (data: z.infer) => { return await apiRequest("POST", "/api/whitelist", data); }, onSuccess: () => { @@ -42,14 +70,12 @@ export default function WhitelistPage() { description: "L'indirizzo IP è stato aggiunto alla whitelist", }); setIsAddDialogOpen(false); - setNewIp(""); - setNewComment(""); - setNewReason(""); + form.reset(); }, - onError: () => { + onError: (error: any) => { toast({ title: "Errore", - description: "Impossibile aggiungere l'IP alla whitelist", + description: error.message || "Impossibile aggiungere l'IP alla whitelist", variant: "destructive", }); }, @@ -66,30 +92,17 @@ export default function WhitelistPage() { description: "L'indirizzo IP è stato rimosso dalla whitelist", }); }, - onError: () => { + onError: (error: any) => { toast({ title: "Errore", - description: "Impossibile rimuovere l'IP dalla whitelist", + description: error.message || "Impossibile rimuovere l'IP dalla whitelist", variant: "destructive", }); }, }); - const handleAdd = () => { - if (!newIp.trim()) { - toast({ - title: "Errore", - description: "Inserisci un indirizzo IP valido", - variant: "destructive", - }); - return; - } - - addMutation.mutate({ - ipAddress: newIp.trim(), - comment: newComment.trim() || undefined, - reason: newReason.trim() || undefined, - }); + const onSubmit = (data: z.infer) => { + addMutation.mutate(data); }; return ( @@ -116,46 +129,62 @@ export default function WhitelistPage() { Inserisci l'indirizzo IP che vuoi proteggere dal blocco automatico -
-
- - setNewIp(e.target.value)} - data-testid="input-ip" +
+ + ( + + Indirizzo IP * + + + + + + )} /> -
-
- - setNewReason(e.target.value)} - data-testid="input-reason" + ( + + Motivo + + + + + + )} /> -
-
- -