Compare commits

...

3 Commits

Author SHA1 Message Date
Marco Lanzara
aa589ab64d 🚀 Release v1.0.108
- Tipo: patch
- Database schema: database-schema/schema.sql (solo struttura)
- Data: 2026-02-14 10:47:54
2026-02-14 10:47:54 +00:00
marco370
dd0e44f78f Add pagination and server-side search to the whitelist page
Implement server-side pagination and search functionality for the whitelist page, including API route updates, storage layer modifications, and frontend enhancements in `Whitelist.tsx`.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 7a657272-55ba-4a79-9a2e-f1ed9bc7a528
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 1971ce5e-1b30-49d5-90b7-63e075ccb563
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/449cf7c4-c97a-45ae-8234-e5c5b8d6a84f/7a657272-55ba-4a79-9a2e-f1ed9bc7a528/RyXWGQA
2026-02-14 10:47:35 +00:00
marco370
1f9ee3919f Improve bulk IP blocking by capturing router connection errors
Improve the bulk IP blocking endpoint to catch and report errors encountered when connecting to MikroTik routers, providing better diagnostics for failed blocks.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 7a657272-55ba-4a79-9a2e-f1ed9bc7a528
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: d822b292-cd84-4c15-8a0f-daabbc03a243
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/449cf7c4-c97a-45ae-8234-e5c5b8d6a84f/7a657272-55ba-4a79-9a2e-f1ed9bc7a528/18EyBWl
2026-02-14 10:17:43 +00:00
6 changed files with 159 additions and 39 deletions

View File

@ -0,0 +1,2 @@
curl -X POST http://localhost:8000/block-all-critical -H "Content-Type: application/json" -d '{"min_score": 80}'
{"message":"Blocco massivo completato: 0 IP bloccati, 260 falliti","blocked":0,"failed":260,"total_critical":260,"details":[{"ip":"157.240.231.60","score":99.99,"status":"failed"},{"ip":"5.134.122.207","score":99.91,"status":"failed"},{"ip":"79.6.115.203","score":99.75,"status":"failed"},{"ip":"37.13.179.85","score":99.15,"status":"failed"},{"ip":"129.69.0.42","score":99.03,"status":"failed"},{"ip":"104.18.36.146","score":97.96,"status":"failed"},{"ip":"88.39.149.52","score":97.14,"status":"failed"},{"ip":"216.58.204.150","score":96.31,"status":"failed"},{"ip":"83.230.138.36","score":96.05,"status":"failed"},{"ip":"80.211.249.119","score":95.86,"status":"failed"},{"ip":"104.18.38.59","score":95.85,"status":"failed"},{"ip":"216.58.204.142","score":95.83,"status":"failed"},{"ip":"185.30.182.159","score":95.56,"status":"failed"},{"ip":"142.251.140.100","score":94.94,"status":"failed"},{"ip":"146.247.137.195","score":94.86,"status":"failed"},{"ip":"172.64.146.98","score":93.8,"status":"failed"},{"ip":"34.252.43.174","score":93.6,"status":"failed"},{"ip":"199.232.194.27","score":93.53,"status":"failed"},{"ip":"151.58.164.255","score":93.49,"status":"failed"},{"ip":"146.247.137.121","score":93.25,"status":"failed"},{"ip":"192.178.202.119","score":92.71,"status":"failed"},{"ip":"195.32.16.194","score":92.39,"status":"failed"},{"ip":"5.90.193.36","score":92.27,"status":"failed"},{"ip":"51.161.172.246","score":92.12,"status":"failed"},{"ip":"83.217.187.128","score":92.05,"status":"failed"},{"ip":"23.216.150.188","score":92.04,"status":"failed"},{"ip":"34.160.109.235","score":91.95,"status":"failed"},{"ip":"13.107.246.43","score":91.93,"status":"failed"},{"ip":"151.101.66.27","score":91.93,"status":"failed"},{"ip":"10.0.237.18","score":91.75,"status":"failed"},{"ip":"5.63.17.10","score":91.66,"status":"failed"},{"ip":"74.125.45.108","score":91.66,"status":"failed"},{"ip":"109.54.106.99","score":91.63,"status":"failed"},{"ip":"103.169.126.50","score":91.49,"status":"failed"},{"ip":"103.169.126.52","score":91.49,"status":"failed"},{"ip":"93.39.92.133","score":91.26,"status":"failed"},{"ip":"103.169.126.16","score":91.18,"status":"failed"},{"ip":"52.123.255.227","score":91.18,"status":"failed"},{"ip":"77.89.41.174","score":91.13,"status":"failed"},{"ip":"93.148.252.209","score":91.12,"status":"failed"},{"ip":"94.101.54.84","score":90.95,"status":"failed"},{"ip":"23.239.11.118","score":90.86,"status":"failed"},{"ip":"52.123.129.14","score":90.66,"status":"failed"},{"ip":"151.78.177.243","score":90.6,"status":"failed"},{"ip":"151.19.103.232","score":90.53,"status":"failed"},{"ip":"35.219.227.195","score":90.29,"status":"failed"},{"ip":"103.169.126.48","score":90.28,"status":"failed"},{"ip":"103.169.126.197","score":90.26,"status":"failed"},{"ip":"151.5.26.99","score":90.24,"status":"failed"},{"ip":"103.169.126.203","score":90.2,"status":"failed"}]}[root@ids ids]#

View File

@ -2,9 +2,9 @@ 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 { Shield, Plus, Trash2, CheckCircle2, XCircle, Search } from "lucide-react"; import { Shield, Plus, Trash2, CheckCircle2, XCircle, Search, ChevronLeft, ChevronRight } from "lucide-react";
import { format } from "date-fns"; import { format } from "date-fns";
import { useState } from "react"; import { useState, useEffect, useMemo } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
@ -31,6 +31,8 @@ import {
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
const ITEMS_PER_PAGE = 50;
const whitelistFormSchema = insertWhitelistSchema.extend({ const whitelistFormSchema = insertWhitelistSchema.extend({
ipAddress: z.string() ipAddress: z.string()
.min(7, "Inserisci un IP valido") .min(7, "Inserisci un IP valido")
@ -41,10 +43,17 @@ const whitelistFormSchema = insertWhitelistSchema.extend({
}, "Ogni ottetto deve essere tra 0 e 255"), }, "Ogni ottetto deve essere tra 0 e 255"),
}); });
interface WhitelistResponse {
items: Whitelist[];
total: number;
}
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 [searchQuery, setSearchQuery] = useState(""); const [searchInput, setSearchInput] = useState("");
const [debouncedSearch, setDebouncedSearch] = useState("");
const [currentPage, setCurrentPage] = useState(1);
const form = useForm<z.infer<typeof whitelistFormSchema>>({ const form = useForm<z.infer<typeof whitelistFormSchema>>({
resolver: zodResolver(whitelistFormSchema), resolver: zodResolver(whitelistFormSchema),
@ -56,16 +65,33 @@ export default function WhitelistPage() {
}, },
}); });
const { data: whitelist, isLoading } = useQuery<Whitelist[]>({ useEffect(() => {
queryKey: ["/api/whitelist"], const timer = setTimeout(() => {
setDebouncedSearch(searchInput);
setCurrentPage(1);
}, 300);
return () => clearTimeout(timer);
}, [searchInput]);
const queryParams = useMemo(() => {
const params = new URLSearchParams();
params.set("limit", ITEMS_PER_PAGE.toString());
params.set("offset", ((currentPage - 1) * ITEMS_PER_PAGE).toString());
if (debouncedSearch.trim()) {
params.set("search", debouncedSearch.trim());
}
return params.toString();
}, [currentPage, debouncedSearch]);
const { data, isLoading } = useQuery<WhitelistResponse>({
queryKey: ["/api/whitelist", currentPage, debouncedSearch],
queryFn: () => fetch(`/api/whitelist?${queryParams}`).then(r => r.json()),
refetchInterval: 10000,
}); });
// Filter whitelist based on search query const whitelistItems = data?.items || [];
const filteredWhitelist = whitelist?.filter((item) => const totalCount = data?.total || 0;
item.ipAddress.toLowerCase().includes(searchQuery.toLowerCase()) || const totalPages = Math.ceil(totalCount / ITEMS_PER_PAGE);
item.reason?.toLowerCase().includes(searchQuery.toLowerCase()) ||
item.comment?.toLowerCase().includes(searchQuery.toLowerCase())
);
const addMutation = useMutation({ const addMutation = useMutation({
mutationFn: async (data: z.infer<typeof whitelistFormSchema>) => { mutationFn: async (data: z.infer<typeof whitelistFormSchema>) => {
@ -203,9 +229,9 @@ export default function WhitelistPage() {
<div className="relative"> <div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" /> <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input <Input
placeholder="Cerca per IP, motivo o note..." placeholder="Cerca per IP, motivo, note o sorgente..."
value={searchQuery} value={searchInput}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchInput(e.target.value)}
className="pl-9" className="pl-9"
data-testid="input-search-whitelist" data-testid="input-search-whitelist"
/> />
@ -215,9 +241,36 @@ export default function WhitelistPage() {
<Card data-testid="card-whitelist"> <Card data-testid="card-whitelist">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center justify-between gap-2 flex-wrap">
<Shield className="h-5 w-5" /> <div className="flex items-center gap-2">
IP Protetti ({filteredWhitelist?.length || 0}{searchQuery && whitelist ? ` di ${whitelist.length}` : ''}) <Shield className="h-5 w-5" />
IP Protetti ({totalCount})
</div>
{totalPages > 1 && (
<div className="flex items-center gap-2 text-sm font-normal">
<Button
variant="outline"
size="icon"
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
disabled={currentPage === 1}
data-testid="button-prev-page"
>
<ChevronLeft className="h-4 w-4" />
</Button>
<span data-testid="text-pagination">
Pagina {currentPage} di {totalPages}
</span>
<Button
variant="outline"
size="icon"
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
disabled={currentPage === totalPages}
data-testid="button-next-page"
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
)}
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@ -225,9 +278,9 @@ export default function WhitelistPage() {
<div className="text-center py-8 text-muted-foreground" data-testid="text-loading"> <div className="text-center py-8 text-muted-foreground" data-testid="text-loading">
Caricamento... Caricamento...
</div> </div>
) : filteredWhitelist && filteredWhitelist.length > 0 ? ( ) : whitelistItems.length > 0 ? (
<div className="space-y-3"> <div className="space-y-3">
{filteredWhitelist.map((item) => ( {whitelistItems.map((item) => (
<div <div
key={item.id} key={item.id}
className="p-4 rounded-lg border hover-elevate" className="p-4 rounded-lg border hover-elevate"
@ -272,12 +325,45 @@ export default function WhitelistPage() {
</div> </div>
</div> </div>
))} ))}
{/* Bottom pagination */}
{totalPages > 1 && (
<div className="flex items-center justify-center gap-4 mt-6 pt-4 border-t">
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
disabled={currentPage === 1}
data-testid="button-prev-page-bottom"
>
<ChevronLeft className="h-4 w-4 mr-1" />
Precedente
</Button>
<span className="text-sm text-muted-foreground" data-testid="text-pagination-bottom">
Pagina {currentPage} di {totalPages} ({totalCount} totali)
</span>
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
disabled={currentPage === totalPages}
data-testid="button-next-page-bottom"
>
Successiva
<ChevronRight className="h-4 w-4 ml-1" />
</Button>
</div>
)}
</div> </div>
) : ( ) : (
<div className="text-center py-12 text-muted-foreground" data-testid="text-empty"> <div className="text-center py-12 text-muted-foreground" data-testid="text-empty">
<Shield className="h-12 w-12 mx-auto mb-4 opacity-50" /> <Shield className="h-12 w-12 mx-auto mb-4 opacity-50" />
<p className="font-medium">Nessun IP in whitelist</p> <p className="font-medium">Nessun IP in whitelist</p>
<p className="text-sm mt-2">Aggiungi indirizzi IP fidati per proteggerli dal blocco automatico</p> {debouncedSearch ? (
<p className="text-sm mt-2">Prova con un altro termine di ricerca</p>
) : (
<p className="text-sm mt-2">Aggiungi indirizzi IP fidati per proteggerli dal blocco automatico</p>
)}
</div> </div>
)} )}
</CardContent> </CardContent>

View File

@ -2,7 +2,7 @@
-- PostgreSQL database dump -- PostgreSQL database dump
-- --
\restrict VfY2jMibUtM7epqSxI0eI9IK5petlVg4wE8lvRgi6qIK19f865uN5QWYQxOQJY8 \restrict bngYLsJZNRm4sGjAcrPW2v7ZwM0VwW8FjcxWfRcYzZDDKc2X525bYmyFEXzl1yh
-- Dumped from database version 16.11 (df20cf9) -- Dumped from database version 16.11 (df20cf9)
-- Dumped by pg_dump version 16.10 -- Dumped by pg_dump version 16.10
@ -387,5 +387,5 @@ ALTER TABLE ONLY public.public_blacklist_ips
-- PostgreSQL database dump complete -- PostgreSQL database dump complete
-- --
\unrestrict VfY2jMibUtM7epqSxI0eI9IK5petlVg4wE8lvRgi6qIK19f865uN5QWYQxOQJY8 \unrestrict bngYLsJZNRm4sGjAcrPW2v7ZwM0VwW8FjcxWfRcYzZDDKc2X525bYmyFEXzl1yh

View File

@ -122,8 +122,12 @@ export async function registerRoutes(app: Express): Promise<Server> {
// Whitelist // Whitelist
app.get("/api/whitelist", async (req, res) => { app.get("/api/whitelist", async (req, res) => {
try { try {
const whitelist = await storage.getAllWhitelist(); const limit = parseInt(req.query.limit as string) || 50;
res.json(whitelist); const offset = parseInt(req.query.offset as string) || 0;
const search = req.query.search as string || undefined;
const result = await storage.getAllWhitelist({ limit, offset, search });
res.json(result);
} catch (error) { } catch (error) {
console.error('[DB ERROR] Failed to fetch whitelist:', error); console.error('[DB ERROR] Failed to fetch whitelist:', error);
res.status(500).json({ error: "Failed to fetch whitelist" }); res.status(500).json({ error: "Failed to fetch whitelist" });
@ -480,7 +484,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
const routers = await storage.getAllRouters(); const routers = await storage.getAllRouters();
const detectionsResult = await storage.getAllDetections({ limit: 1000 }); const detectionsResult = await storage.getAllDetections({ limit: 1000 });
const recentLogs = await storage.getRecentLogs(1000); const recentLogs = await storage.getRecentLogs(1000);
const whitelist = await storage.getAllWhitelist(); const whitelistResult = await storage.getAllWhitelist({ limit: 1 });
const latestTraining = await storage.getLatestTraining(); const latestTraining = await storage.getLatestTraining();
const detectionsList = detectionsResult.detections; const detectionsList = detectionsResult.detections;
@ -503,7 +507,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
recent: recentLogs.length recent: recentLogs.length
}, },
whitelist: { whitelist: {
total: whitelist.length total: whitelistResult.total
}, },
latestTraining: latestTraining latestTraining: latestTraining
}); });

View File

@ -55,7 +55,7 @@ export interface IStorage {
getUnblockedDetections(): Promise<Detection[]>; getUnblockedDetections(): Promise<Detection[]>;
// Whitelist // Whitelist
getAllWhitelist(): Promise<Whitelist[]>; getAllWhitelist(options?: { limit?: number; offset?: number; search?: string }): Promise<{ items: Whitelist[]; total: number }>;
getWhitelistByIp(ipAddress: string): Promise<Whitelist | undefined>; getWhitelistByIp(ipAddress: string): Promise<Whitelist | undefined>;
createWhitelist(whitelist: InsertWhitelist): Promise<Whitelist>; createWhitelist(whitelist: InsertWhitelist): Promise<Whitelist>;
deleteWhitelist(id: string): Promise<boolean>; deleteWhitelist(id: string): Promise<boolean>;
@ -271,12 +271,40 @@ export class DatabaseStorage implements IStorage {
} }
// Whitelist // Whitelist
async getAllWhitelist(): Promise<Whitelist[]> { async getAllWhitelist(options?: { limit?: number; offset?: number; search?: string }): Promise<{ items: Whitelist[]; total: number }> {
return await db const limit = options?.limit || 50;
const offset = options?.offset || 0;
const search = options?.search?.trim().toLowerCase();
const conditions: any[] = [eq(whitelist.active, true)];
if (search) {
conditions.push(
sql`(
LOWER(${whitelist.ipAddress}) LIKE ${'%' + search + '%'}
OR LOWER(COALESCE(${whitelist.reason}, '')) LIKE ${'%' + search + '%'}
OR LOWER(COALESCE(${whitelist.comment}, '')) LIKE ${'%' + search + '%'}
OR LOWER(COALESCE(${whitelist.source}, '')) LIKE ${'%' + search + '%'}
)`
);
}
const whereClause = and(...conditions);
const [countResult] = await db
.select({ count: sql<number>`cast(count(*) as integer)` })
.from(whitelist)
.where(whereClause);
const items = await db
.select() .select()
.from(whitelist) .from(whitelist)
.where(eq(whitelist.active, true)) .where(whereClause)
.orderBy(desc(whitelist.createdAt)); .orderBy(desc(whitelist.createdAt))
.limit(limit)
.offset(offset);
return { items, total: countResult.count };
} }
async getWhitelistByIp(ipAddress: string): Promise<Whitelist | undefined> { async getWhitelistByIp(ipAddress: string): Promise<Whitelist | undefined> {

View File

@ -1,7 +1,13 @@
{ {
"version": "1.0.107", "version": "1.0.108",
"lastUpdate": "2026-02-14T10:15:20.502Z", "lastUpdate": "2026-02-14T10:47:54.686Z",
"changelog": [ "changelog": [
{
"version": "1.0.108",
"date": "2026-02-14",
"type": "patch",
"description": "Deployment automatico v1.0.108"
},
{ {
"version": "1.0.107", "version": "1.0.107",
"date": "2026-02-14", "date": "2026-02-14",
@ -295,12 +301,6 @@
"date": "2025-11-24", "date": "2025-11-24",
"type": "patch", "type": "patch",
"description": "Deployment automatico v1.0.59" "description": "Deployment automatico v1.0.59"
},
{
"version": "1.0.58",
"date": "2025-11-24",
"type": "patch",
"description": "Deployment automatico v1.0.58"
} }
] ]
} }