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
This commit is contained in:
parent
7171c3d607
commit
7b514f470f
4
.replit
4
.replit
@ -15,8 +15,8 @@ localPort = 5000
|
|||||||
externalPort = 80
|
externalPort = 80
|
||||||
|
|
||||||
[[ports]]
|
[[ports]]
|
||||||
localPort = 43079
|
localPort = 41303
|
||||||
externalPort = 3001
|
externalPort = 3002
|
||||||
|
|
||||||
[[ports]]
|
[[ports]]
|
||||||
localPort = 43803
|
localPort = 43803
|
||||||
|
|||||||
@ -8,8 +8,9 @@ import { format } from "date-fns";
|
|||||||
import type { TrainingHistory } from "@shared/schema";
|
import type { TrainingHistory } from "@shared/schema";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Label } from "@/components/ui/label";
|
import { useForm } from "react-hook-form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@ -19,6 +20,17 @@ import {
|
|||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
} from "@/components/ui/dialog";
|
} 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 {
|
interface MLStatsResponse {
|
||||||
logs?: { total: number; last_hour: number };
|
logs?: { total: number; last_hour: number };
|
||||||
@ -27,16 +39,40 @@ interface MLStatsResponse {
|
|||||||
latest_training?: any;
|
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() {
|
export default function TrainingPage() {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [isTrainDialogOpen, setIsTrainDialogOpen] = useState(false);
|
const [isTrainDialogOpen, setIsTrainDialogOpen] = useState(false);
|
||||||
const [isDetectDialogOpen, setIsDetectDialogOpen] = useState(false);
|
const [isDetectDialogOpen, setIsDetectDialogOpen] = useState(false);
|
||||||
const [trainRecords, setTrainRecords] = useState("100000");
|
|
||||||
const [trainHours, setTrainHours] = useState("24");
|
const trainForm = useForm<z.infer<typeof trainFormSchema>>({
|
||||||
const [detectRecords, setDetectRecords] = useState("50000");
|
resolver: zodResolver(trainFormSchema),
|
||||||
const [detectHours, setDetectHours] = useState("1");
|
defaultValues: {
|
||||||
const [detectThreshold, setDetectThreshold] = useState("75");
|
max_records: 100000,
|
||||||
const [detectAutoBlock, setDetectAutoBlock] = useState(true);
|
hours_back: 24,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const detectForm = useForm<z.infer<typeof detectFormSchema>>({
|
||||||
|
resolver: zodResolver(detectFormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
max_records: 50000,
|
||||||
|
hours_back: 1,
|
||||||
|
risk_threshold: 75,
|
||||||
|
auto_block: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const { data: history, isLoading } = useQuery<TrainingHistory[]>({
|
const { data: history, isLoading } = useQuery<TrainingHistory[]>({
|
||||||
queryKey: ["/api/training-history"],
|
queryKey: ["/api/training-history"],
|
||||||
@ -49,7 +85,7 @@ export default function TrainingPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const trainMutation = useMutation({
|
const trainMutation = useMutation({
|
||||||
mutationFn: async (params: { max_records: number; hours_back: number }) => {
|
mutationFn: async (params: z.infer<typeof trainFormSchema>) => {
|
||||||
return await apiRequest("POST", "/api/ml/train", params);
|
return await apiRequest("POST", "/api/ml/train", params);
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@ -60,18 +96,19 @@ export default function TrainingPage() {
|
|||||||
description: "Il modello ML è in addestramento. Controlla lo storico tra qualche minuto.",
|
description: "Il modello ML è in addestramento. Controlla lo storico tra qualche minuto.",
|
||||||
});
|
});
|
||||||
setIsTrainDialogOpen(false);
|
setIsTrainDialogOpen(false);
|
||||||
|
trainForm.reset();
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: (error: any) => {
|
||||||
toast({
|
toast({
|
||||||
title: "Errore",
|
title: "Errore",
|
||||||
description: "Impossibile avviare il training",
|
description: error.message || "Impossibile avviare il training",
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const detectMutation = useMutation({
|
const detectMutation = useMutation({
|
||||||
mutationFn: async (params: { max_records: number; hours_back: number; risk_threshold: number; auto_block: boolean }) => {
|
mutationFn: async (params: z.infer<typeof detectFormSchema>) => {
|
||||||
return await apiRequest("POST", "/api/ml/detect", params);
|
return await apiRequest("POST", "/api/ml/detect", params);
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@ -82,52 +119,23 @@ export default function TrainingPage() {
|
|||||||
description: "Analisi anomalie in corso. Controlla i rilevamenti tra qualche secondo.",
|
description: "Analisi anomalie in corso. Controlla i rilevamenti tra qualche secondo.",
|
||||||
});
|
});
|
||||||
setIsDetectDialogOpen(false);
|
setIsDetectDialogOpen(false);
|
||||||
|
detectForm.reset();
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: (error: any) => {
|
||||||
toast({
|
toast({
|
||||||
title: "Errore",
|
title: "Errore",
|
||||||
description: "Impossibile avviare la detection",
|
description: error.message || "Impossibile avviare la detection",
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleTrain = () => {
|
const onTrainSubmit = (data: z.infer<typeof trainFormSchema>) => {
|
||||||
const records = parseInt(trainRecords);
|
trainMutation.mutate(data);
|
||||||
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 handleDetect = () => {
|
const onDetectSubmit = (data: z.infer<typeof detectFormSchema>) => {
|
||||||
const records = parseInt(detectRecords);
|
detectMutation.mutate(data);
|
||||||
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,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -213,38 +221,51 @@ export default function TrainingPage() {
|
|||||||
Configura i parametri per l'addestramento del modello
|
Configura i parametri per l'addestramento del modello
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4 py-4">
|
<Form {...trainForm}>
|
||||||
<div className="space-y-2">
|
<form onSubmit={trainForm.handleSubmit(onTrainSubmit)} className="space-y-4 py-4">
|
||||||
<Label htmlFor="train-records">Numero Record</Label>
|
<FormField
|
||||||
<Input
|
control={trainForm.control}
|
||||||
id="train-records"
|
name="max_records"
|
||||||
type="number"
|
render={({ field }) => (
|
||||||
value={trainRecords}
|
<FormItem>
|
||||||
onChange={(e) => setTrainRecords(e.target.value)}
|
<FormLabel>Numero Record</FormLabel>
|
||||||
data-testid="input-train-records"
|
<FormControl>
|
||||||
|
<Input type="number" {...field} data-testid="input-train-records" />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>Consigliato: 100000</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">Consigliato: 100000</p>
|
<FormField
|
||||||
</div>
|
control={trainForm.control}
|
||||||
<div className="space-y-2">
|
name="hours_back"
|
||||||
<Label htmlFor="train-hours">Ore Precedenti</Label>
|
render={({ field }) => (
|
||||||
<Input
|
<FormItem>
|
||||||
id="train-hours"
|
<FormLabel>Ore Precedenti</FormLabel>
|
||||||
type="number"
|
<FormControl>
|
||||||
value={trainHours}
|
<Input type="number" {...field} data-testid="input-train-hours" />
|
||||||
onChange={(e) => setTrainHours(e.target.value)}
|
</FormControl>
|
||||||
data-testid="input-train-hours"
|
<FormDescription>Consigliato: 24</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">Consigliato: 24</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => setIsTrainDialogOpen(false)} data-testid="button-cancel-training">
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setIsTrainDialogOpen(false)}
|
||||||
|
data-testid="button-cancel-training"
|
||||||
|
>
|
||||||
Annulla
|
Annulla
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleTrain} disabled={trainMutation.isPending} data-testid="button-confirm-training">
|
<Button type="submit" disabled={trainMutation.isPending} data-testid="button-confirm-training">
|
||||||
{trainMutation.isPending ? "Avvio..." : "Avvia Training"}
|
{trainMutation.isPending ? "Avvio..." : "Avvia Training"}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@ -275,62 +296,83 @@ export default function TrainingPage() {
|
|||||||
Configura i parametri per il rilevamento anomalie
|
Configura i parametri per il rilevamento anomalie
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4 py-4">
|
<Form {...detectForm}>
|
||||||
<div className="space-y-2">
|
<form onSubmit={detectForm.handleSubmit(onDetectSubmit)} className="space-y-4 py-4">
|
||||||
<Label htmlFor="detect-records">Numero Record</Label>
|
<FormField
|
||||||
<Input
|
control={detectForm.control}
|
||||||
id="detect-records"
|
name="max_records"
|
||||||
type="number"
|
render={({ field }) => (
|
||||||
value={detectRecords}
|
<FormItem>
|
||||||
onChange={(e) => setDetectRecords(e.target.value)}
|
<FormLabel>Numero Record</FormLabel>
|
||||||
data-testid="input-detect-records"
|
<FormControl>
|
||||||
|
<Input type="number" {...field} data-testid="input-detect-records" />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>Consigliato: 50000</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">Consigliato: 50000</p>
|
<FormField
|
||||||
</div>
|
control={detectForm.control}
|
||||||
<div className="space-y-2">
|
name="hours_back"
|
||||||
<Label htmlFor="detect-hours">Ore Precedenti</Label>
|
render={({ field }) => (
|
||||||
<Input
|
<FormItem>
|
||||||
id="detect-hours"
|
<FormLabel>Ore Precedenti</FormLabel>
|
||||||
type="number"
|
<FormControl>
|
||||||
value={detectHours}
|
<Input type="number" {...field} data-testid="input-detect-hours" />
|
||||||
onChange={(e) => setDetectHours(e.target.value)}
|
</FormControl>
|
||||||
data-testid="input-detect-hours"
|
<FormDescription>Consigliato: 1</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">Consigliato: 1</p>
|
<FormField
|
||||||
</div>
|
control={detectForm.control}
|
||||||
<div className="space-y-2">
|
name="risk_threshold"
|
||||||
<Label htmlFor="detect-threshold">Soglia Rischio (%)</Label>
|
render={({ field }) => (
|
||||||
<Input
|
<FormItem>
|
||||||
id="detect-threshold"
|
<FormLabel>Soglia Rischio (%)</FormLabel>
|
||||||
type="number"
|
<FormControl>
|
||||||
min="0"
|
<Input type="number" min="0" max="100" {...field} data-testid="input-detect-threshold" />
|
||||||
max="100"
|
</FormControl>
|
||||||
value={detectThreshold}
|
<FormDescription>Consigliato: 75</FormDescription>
|
||||||
onChange={(e) => setDetectThreshold(e.target.value)}
|
<FormMessage />
|
||||||
data-testid="input-detect-threshold"
|
</FormItem>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">Consigliato: 75</p>
|
<FormField
|
||||||
</div>
|
control={detectForm.control}
|
||||||
<div className="flex items-center space-x-2">
|
name="auto_block"
|
||||||
<input
|
render={({ field }) => (
|
||||||
type="checkbox"
|
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
|
||||||
id="auto-block"
|
<FormControl>
|
||||||
checked={detectAutoBlock}
|
<Checkbox
|
||||||
onChange={(e) => setDetectAutoBlock(e.target.checked)}
|
checked={field.value}
|
||||||
className="rounded border-gray-300"
|
onCheckedChange={field.onChange}
|
||||||
data-testid="checkbox-auto-block"
|
data-testid="checkbox-auto-block"
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="auto-block">Blocco automatico IP critici (≥80)</Label>
|
</FormControl>
|
||||||
</div>
|
<div className="space-y-1 leading-none">
|
||||||
|
<FormLabel>Blocco automatico IP critici (≥80)</FormLabel>
|
||||||
</div>
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => setIsDetectDialogOpen(false)} data-testid="button-cancel-detection">
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setIsDetectDialogOpen(false)}
|
||||||
|
data-testid="button-cancel-detection"
|
||||||
|
>
|
||||||
Annulla
|
Annulla
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleDetect} disabled={detectMutation.isPending} data-testid="button-confirm-detection">
|
<Button type="submit" disabled={detectMutation.isPending} data-testid="button-confirm-detection">
|
||||||
{detectMutation.isPending ? "Avvio..." : "Avvia Detection"}
|
{detectMutation.isPending ? "Avvio..." : "Avvia Detection"}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@ -2,13 +2,14 @@ import { useQuery, useMutation } from "@tanstack/react-query";
|
|||||||
import { queryClient, apiRequest } from "@/lib/queryClient";
|
import { queryClient, apiRequest } from "@/lib/queryClient";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
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 { Shield, Plus, Trash2, CheckCircle2, XCircle } from "lucide-react";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { useState } from "react";
|
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 type { Whitelist } from "@shared/schema";
|
||||||
|
import { insertWhitelistSchema } from "@shared/schema";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@ -19,20 +20,47 @@ import {
|
|||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
} from "@/components/ui/dialog";
|
} 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() {
|
export default function WhitelistPage() {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
|
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
|
||||||
const [newIp, setNewIp] = useState("");
|
|
||||||
const [newComment, setNewComment] = useState("");
|
const form = useForm<z.infer<typeof whitelistFormSchema>>({
|
||||||
const [newReason, setNewReason] = useState("");
|
resolver: zodResolver(whitelistFormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
ipAddress: "",
|
||||||
|
comment: "",
|
||||||
|
reason: "",
|
||||||
|
active: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const { data: whitelist, isLoading } = useQuery<Whitelist[]>({
|
const { data: whitelist, isLoading } = useQuery<Whitelist[]>({
|
||||||
queryKey: ["/api/whitelist"],
|
queryKey: ["/api/whitelist"],
|
||||||
});
|
});
|
||||||
|
|
||||||
const addMutation = useMutation({
|
const addMutation = useMutation({
|
||||||
mutationFn: async (data: { ipAddress: string; comment?: string; reason?: string }) => {
|
mutationFn: async (data: z.infer<typeof whitelistFormSchema>) => {
|
||||||
return await apiRequest("POST", "/api/whitelist", data);
|
return await apiRequest("POST", "/api/whitelist", data);
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@ -42,14 +70,12 @@ export default function WhitelistPage() {
|
|||||||
description: "L'indirizzo IP è stato aggiunto alla whitelist",
|
description: "L'indirizzo IP è stato aggiunto alla whitelist",
|
||||||
});
|
});
|
||||||
setIsAddDialogOpen(false);
|
setIsAddDialogOpen(false);
|
||||||
setNewIp("");
|
form.reset();
|
||||||
setNewComment("");
|
|
||||||
setNewReason("");
|
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: (error: any) => {
|
||||||
toast({
|
toast({
|
||||||
title: "Errore",
|
title: "Errore",
|
||||||
description: "Impossibile aggiungere l'IP alla whitelist",
|
description: error.message || "Impossibile aggiungere l'IP alla whitelist",
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -66,30 +92,17 @@ export default function WhitelistPage() {
|
|||||||
description: "L'indirizzo IP è stato rimosso dalla whitelist",
|
description: "L'indirizzo IP è stato rimosso dalla whitelist",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: (error: any) => {
|
||||||
toast({
|
toast({
|
||||||
title: "Errore",
|
title: "Errore",
|
||||||
description: "Impossibile rimuovere l'IP dalla whitelist",
|
description: error.message || "Impossibile rimuovere l'IP dalla whitelist",
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleAdd = () => {
|
const onSubmit = (data: z.infer<typeof whitelistFormSchema>) => {
|
||||||
if (!newIp.trim()) {
|
addMutation.mutate(data);
|
||||||
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,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -116,46 +129,62 @@ export default function WhitelistPage() {
|
|||||||
Inserisci l'indirizzo IP che vuoi proteggere dal blocco automatico
|
Inserisci l'indirizzo IP che vuoi proteggere dal blocco automatico
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4 py-4">
|
<Form {...form}>
|
||||||
<div className="space-y-2">
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4 py-4">
|
||||||
<Label htmlFor="ip">Indirizzo IP *</Label>
|
<FormField
|
||||||
<Input
|
control={form.control}
|
||||||
id="ip"
|
name="ipAddress"
|
||||||
placeholder="192.168.1.100"
|
render={({ field }) => (
|
||||||
value={newIp}
|
<FormItem>
|
||||||
onChange={(e) => setNewIp(e.target.value)}
|
<FormLabel>Indirizzo IP *</FormLabel>
|
||||||
data-testid="input-ip"
|
<FormControl>
|
||||||
|
<Input placeholder="192.168.1.100" {...field} data-testid="input-ip" />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
<FormField
|
||||||
<div className="space-y-2">
|
control={form.control}
|
||||||
<Label htmlFor="reason">Motivo</Label>
|
name="reason"
|
||||||
<Input
|
render={({ field }) => (
|
||||||
id="reason"
|
<FormItem>
|
||||||
placeholder="Es: Server backup"
|
<FormLabel>Motivo</FormLabel>
|
||||||
value={newReason}
|
<FormControl>
|
||||||
onChange={(e) => setNewReason(e.target.value)}
|
<Input placeholder="Es: Server backup" {...field} data-testid="input-reason" />
|
||||||
data-testid="input-reason"
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
<FormField
|
||||||
<div className="space-y-2">
|
control={form.control}
|
||||||
<Label htmlFor="comment">Note</Label>
|
name="comment"
|
||||||
<Textarea
|
render={({ field }) => (
|
||||||
id="comment"
|
<FormItem>
|
||||||
placeholder="Note aggiuntive..."
|
<FormLabel>Note</FormLabel>
|
||||||
value={newComment}
|
<FormControl>
|
||||||
onChange={(e) => setNewComment(e.target.value)}
|
<Textarea placeholder="Note aggiuntive..." {...field} data-testid="input-comment" />
|
||||||
data-testid="input-comment"
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => setIsAddDialogOpen(false)} data-testid="button-cancel">
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setIsAddDialogOpen(false)}
|
||||||
|
data-testid="button-cancel"
|
||||||
|
>
|
||||||
Annulla
|
Annulla
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleAdd} disabled={addMutation.isPending} data-testid="button-confirm-add">
|
<Button type="submit" disabled={addMutation.isPending} data-testid="button-confirm-add">
|
||||||
{addMutation.isPending ? "Aggiunta..." : "Aggiungi"}
|
{addMutation.isPending ? "Aggiunta..." : "Aggiungi"}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -167,24 +167,51 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ML Actions - Trigger training/detection on Python backend
|
// ML Actions - Trigger training/detection on Python backend
|
||||||
|
const ML_BACKEND_URL = process.env.ML_BACKEND_URL || "http://localhost:8000";
|
||||||
|
const ML_TIMEOUT = 120000; // 2 minutes timeout
|
||||||
|
|
||||||
app.post("/api/ml/train", async (req, res) => {
|
app.post("/api/ml/train", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { max_records = 100000, hours_back = 24 } = req.body;
|
const { max_records = 100000, hours_back = 24 } = req.body;
|
||||||
|
|
||||||
const response = await fetch("http://localhost:8000/train", {
|
// Validate input
|
||||||
|
if (typeof max_records !== 'number' || max_records <= 0 || max_records > 1000000) {
|
||||||
|
return res.status(400).json({ error: "max_records must be between 1 and 1000000" });
|
||||||
|
}
|
||||||
|
if (typeof hours_back !== 'number' || hours_back <= 0 || hours_back > 720) {
|
||||||
|
return res.status(400).json({ error: "hours_back must be between 1 and 720" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeout = setTimeout(() => controller.abort(), ML_TIMEOUT);
|
||||||
|
|
||||||
|
const response = await fetch(`${ML_BACKEND_URL}/train`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ max_records, hours_back }),
|
body: JSON.stringify({ max_records, hours_back }),
|
||||||
|
signal: controller.signal,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
clearTimeout(timeout);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error("Python backend training failed");
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
return res.status(response.status).json({
|
||||||
|
error: errorData.detail || "Training failed",
|
||||||
|
status: response.status,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
res.json(data);
|
res.json(data);
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
res.status(500).json({ error: "Failed to trigger training" });
|
if (error.name === 'AbortError') {
|
||||||
|
return res.status(504).json({ error: "Training timeout - operation took too long" });
|
||||||
|
}
|
||||||
|
if (error.code === 'ECONNREFUSED') {
|
||||||
|
return res.status(503).json({ error: "ML backend not available - is Python server running?" });
|
||||||
|
}
|
||||||
|
res.status(500).json({ error: error.message || "Failed to trigger training" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -192,35 +219,79 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
try {
|
try {
|
||||||
const { max_records = 50000, hours_back = 1, risk_threshold = 75, auto_block = false } = req.body;
|
const { max_records = 50000, hours_back = 1, risk_threshold = 75, auto_block = false } = req.body;
|
||||||
|
|
||||||
const response = await fetch("http://localhost:8000/detect", {
|
// Validate input
|
||||||
|
if (typeof max_records !== 'number' || max_records <= 0 || max_records > 1000000) {
|
||||||
|
return res.status(400).json({ error: "max_records must be between 1 and 1000000" });
|
||||||
|
}
|
||||||
|
if (typeof hours_back !== 'number' || hours_back <= 0 || hours_back > 720) {
|
||||||
|
return res.status(400).json({ error: "hours_back must be between 1 and 720" });
|
||||||
|
}
|
||||||
|
if (typeof risk_threshold !== 'number' || risk_threshold < 0 || risk_threshold > 100) {
|
||||||
|
return res.status(400).json({ error: "risk_threshold must be between 0 and 100" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeout = setTimeout(() => controller.abort(), ML_TIMEOUT);
|
||||||
|
|
||||||
|
const response = await fetch(`${ML_BACKEND_URL}/detect`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ max_records, hours_back, risk_threshold, auto_block }),
|
body: JSON.stringify({ max_records, hours_back, risk_threshold, auto_block }),
|
||||||
|
signal: controller.signal,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
clearTimeout(timeout);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error("Python backend detection failed");
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
return res.status(response.status).json({
|
||||||
|
error: errorData.detail || "Detection failed",
|
||||||
|
status: response.status,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
res.json(data);
|
res.json(data);
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
res.status(500).json({ error: "Failed to trigger detection" });
|
if (error.name === 'AbortError') {
|
||||||
|
return res.status(504).json({ error: "Detection timeout - operation took too long" });
|
||||||
|
}
|
||||||
|
if (error.code === 'ECONNREFUSED') {
|
||||||
|
return res.status(503).json({ error: "ML backend not available - is Python server running?" });
|
||||||
|
}
|
||||||
|
res.status(500).json({ error: error.message || "Failed to trigger detection" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get("/api/ml/stats", async (req, res) => {
|
app.get("/api/ml/stats", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("http://localhost:8000/stats");
|
const controller = new AbortController();
|
||||||
|
const timeout = setTimeout(() => controller.abort(), 10000); // 10s timeout for stats
|
||||||
|
|
||||||
|
const response = await fetch(`${ML_BACKEND_URL}/stats`, {
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
clearTimeout(timeout);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error("Python backend stats failed");
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
return res.status(response.status).json({
|
||||||
|
error: errorData.detail || "Failed to fetch ML stats",
|
||||||
|
status: response.status,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
res.json(data);
|
res.json(data);
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
res.status(500).json({ error: "Failed to fetch ML stats" });
|
if (error.name === 'AbortError') {
|
||||||
|
return res.status(504).json({ error: "Stats timeout" });
|
||||||
|
}
|
||||||
|
if (error.code === 'ECONNREFUSED') {
|
||||||
|
return res.status(503).json({ error: "ML backend not available" });
|
||||||
|
}
|
||||||
|
res.status(500).json({ error: error.message || "Failed to fetch ML stats" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user