Security added on delete service and list all node + cleaning some code

This commit is contained in:
Blomios
2026-01-07 22:16:34 +01:00
parent 3c8bebb2ad
commit a64b10175e
192 changed files with 45470 additions and 4308 deletions

View File

@ -5,6 +5,9 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Index from "./pages/Index";
import Admin from "./pages/Admin";
import Login from "./pages/Login";
import Users from "./pages/Users";
import ProtectedRoute from "./components/ProtectedRoute"
import NotFound from "./pages/NotFound";
const queryClient = new QueryClient();
@ -17,7 +20,11 @@ const App = () => (
<BrowserRouter>
<Routes>
<Route path="/" element={<Index />} />
<Route path="/admin" element={<Admin />} />
<Route path="/login" element={<Login />} />
<Route path="/admin" element={
<Admin />
} />
<Route path="/admin/users" element={<Users />} />
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>

View File

@ -1,5 +1,5 @@
import { NavLink } from '@/components/NavLink';
import { Activity, Settings, Server, Menu, X } from 'lucide-react';
import { Activity, Settings, Server, Menu, X, Users, LogIn } from 'lucide-react';
import { cn } from '@/lib/utils';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
@ -53,6 +53,30 @@ export function Layout({ children }: LayoutProps) {
<Settings className="w-4 h-4" />
Administration
</NavLink>
<NavLink
to="/admin/users"
className={cn(
'flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium',
'text-muted-foreground hover:text-foreground hover:bg-muted/50',
'transition-colors'
)}
activeClassName="bg-muted text-foreground"
>
<Users className="w-4 h-4" />
Utilisateurs
</NavLink>
<NavLink
to="/login"
className={cn(
'flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium',
'text-muted-foreground hover:text-foreground hover:bg-muted/50',
'transition-colors'
)}
activeClassName="bg-muted text-foreground"
>
<LogIn className="w-4 h-4" />
Connexion
</NavLink>
</nav>
{/* Mobile Menu Button */}
@ -97,6 +121,32 @@ export function Layout({ children }: LayoutProps) {
<Settings className="w-5 h-5" />
Administration
</NavLink>
<NavLink
to="/admin/users"
className={cn(
'flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium w-full',
'text-muted-foreground hover:text-foreground hover:bg-muted/50',
'transition-colors'
)}
activeClassName="bg-muted text-foreground"
onClick={() => setMobileMenuOpen(false)}
>
<Users className="w-5 h-5" />
Utilisateurs
</NavLink>
<NavLink
to="/login"
className={cn(
'flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium w-full',
'text-muted-foreground hover:text-foreground hover:bg-muted/50',
'transition-colors'
)}
activeClassName="bg-muted text-foreground"
onClick={() => setMobileMenuOpen(false)}
>
<LogIn className="w-5 h-5" />
Connexion
</NavLink>
</div>
</nav>
)}

View File

@ -0,0 +1,17 @@
import { Navigate } from 'react-router-dom';
const ProtectedRoute = ({ children }) => {
// On vérifie si un cookie ou une info de session existe
// Note : Avec les sessions, le plus sûr est de vérifier un état 'isAuthenticated'
// mis à jour après le login ou via un appel de vérification au démarrage.
const isAuthenticated = localStorage.getItem('isLoggedIn') === 'true';
if (!isAuthenticated) {
// Redirige vers login si non connecté
return <Navigate to="/login" replace />;
}
return children;
};
export default ProtectedRoute;

View File

@ -1,5 +1,6 @@
import { Node, Service, ServiceStatus, StatusRecord, STATUS_CODE_MAP, DEFAULT_STATUS } from '@/types/monitoring';
import { useEffect, useState } from 'react';
import api from '@/lib/axios';
const API_URL = '/api';
@ -9,9 +10,6 @@ interface HistoryRecordAPI {
}
interface ServiceAPI extends Omit<Service, 'status' | 'history'> {
// Le statut courant du service n'est peut-être pas envoyé par l'API
// Si l'API envoie un code numérique ici aussi, ajustez le type:
// status: number;
history: {
minute: HistoryRecordAPI[];
hour: HistoryRecordAPI[];
@ -21,9 +19,6 @@ interface ServiceAPI extends Omit<Service, 'status' | 'history'> {
export interface NodeAPI extends Omit<Node, 'services' | 'status' | 'lastSeen'> {
services: ServiceAPI[];
// Si l'API envoie aussi un statut pour le Node:
//status: number;
//lastSeen: string; // Car la date est une chaîne dans le JSON brut
}
const mapStatusCode = (code: number): ServiceStatus => {
@ -34,75 +29,59 @@ const mapStatusCode = (code: number): ServiceStatus => {
const transformHistoryRecords = (records: HistoryRecordAPI[]): StatusRecord[] => {
return records.map(record => ({
// Conversion de la chaîne ISO 8601 en objet Date JavaScript
timestamp: new Date(record.timestamp),
// Conversion du code numérique en ServiceStatus sémantique (chaîne)
status: mapStatusCode(record.status),
}));
};
export function useNodeUpdater(onUpdate?: (data: Node[]) => void) {
const [isLoading, setIsLoading] = useState(false); // Commence à false, car on peut l'appeler manuellement
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// Fonction de chargement qui peut être appelée manuellement ou au démarrage
const fetchAndApplyNodes = async () => {
setIsLoading(true);
setError(null);
try {
const response = await fetch("/api/retrieveNodeList");
if (!response.ok) {
throw new Error(`Erreur HTTP: ${response.status}`);
}
const response = await api.get<Record<string, NodeAPI>>("/retrieveNodeList");
const rawObject: Record<string, NodeAPI> = await response.json();
const rawObject = response.data;
const rawDataArray: NodeAPI[] = Object.values(rawObject);
// 3. Transformation des données
const convertedData: Node[] = rawDataArray.map((node: NodeAPI) => {
// Transformation de CHAQUE service
const transformedServices: Service[] = node.services.map(service => {
// 1. Transformation de l'historique
const transformedHistory = {
minute: transformHistoryRecords(service.history.minute),
hour: transformHistoryRecords(service.history.hour),
day: transformHistoryRecords(service.history.day),
};
// 2. Détermination du statut courant du service
// Nous prenons le dernier statut enregistré dans l'historique 'minute'
const currentStatusCode = service.history.minute[0]?.status ?? -1;
const currentStatus = mapStatusCode(currentStatusCode);
return {
...service,
// Assignation du statut sémantique calculé
status: currentStatus,
history: transformedHistory,
} as Service; // Caster explicitement si les types intermédiaires le nécessitent
} as Service;
});
// Retourne le node transformé, respectant l'interface Node
return {
...node,
id: node.id, // Assurez-vous que ces champs sont présents et corrects
id: node.id,
name: node.name,
address: node.address,
// Statut global du Node
status: 'operational',
services: transformedServices,
// Date de réception de la donnée (pour l'affichage "dernière vue")
lastSeen: new Date(Date.now()),
} as Node;
});
// Mettre à jour la variable globale
mockNodes = convertedData;
if (onUpdate) {
onUpdate(convertedData); // C'est CA qui va déclencher le refresh !
onUpdate(convertedData);
}
} catch (err: any) {
@ -113,13 +92,10 @@ export function useNodeUpdater(onUpdate?: (data: Node[]) => void) {
}
};
// Vous pouvez utiliser useEffect ici si vous voulez que le chargement se fasse au montage
useEffect(() => {
// Appelle la fonction de chargement au montage du Hook
fetchAndApplyNodes();
}, []);
// Retourne les infos d'état et la fonction de rafraîchissement
return { isLoading, error, fetchAndApplyNodes };
}

20
frontend/src/lib/axios.ts Normal file
View File

@ -0,0 +1,20 @@
import axios, { AxiosInstance, AxiosError } from 'axios';
// On définit l'instance avec le type AxiosInstance
const api: AxiosInstance = axios.create({
baseURL: '/api',
withCredentials: true
});
api.interceptors.response.use(
(response) => response,
(error: AxiosError) => {
if (error.response && error.response.status === 401) {
localStorage.removeItem('isLoggedIn');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
export default api;

View File

@ -2,8 +2,9 @@ import { useState, useEffect } from 'react';
import { Layout } from '@/components/Layout';
import { NodeAdminCard } from '@/components/admin/NodeAdminCard';
import { mockNodes, useNodeUpdater } from '@/data/mockData';
import { Node, Service } from '@/types/monitoring';
import { Node, Service, NodeDeleteRequest } from '@/types/monitoring';
import { useToast } from '@/hooks/use-toast';
import api from '@/lib/axios';
const Admin = () => {
const [nodes, setNodes] = useState<Node[]>(mockNodes);
@ -42,37 +43,28 @@ const Admin = () => {
const removeService = async () => {
try {
const response = await fetch(`/api/deleteService`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
// 'Authorization': 'Bearer VOTRE_TOKEN' (si besoin)
},
body: JSON.stringify({
node_id: nodeId, // l'ID du node (ex: 1)
service_id: serviceId // l'ID du service (ex: 42)
}),
const response = await api.delete<void>(`/deleteService`, {
data: {
node_id: nodeId,
service_id: serviceId
} as NodeDeleteRequest
});
if (!response.ok) {
throw new Error(`Erreur HTTP: ${response.status}`);
}else {
setNodes(prevNodes =>
prevNodes.map(node =>
node.id === nodeId
? { ...node, services: node.services.filter(s => s.id !== serviceId) }
: node
)
);
toast({
title: 'Service supprimé',
description: 'Le service a été supprimé avec succès.',
variant: 'destructive',
});
}
setNodes(prevNodes =>
prevNodes.map(node =>
node.id === nodeId
? { ...node, services: node.services.filter(s => s.id !== serviceId) }
: node
)
);
toast({
title: 'Service supprimé',
description: 'Le service a été supprimé avec succès.',
variant: 'destructive',
});
} catch (err: any) {
console.error("Erreur lors de la mise à jour:Ajouter /api devant toutes tes routes dans ton routeur Go.", err);
console.error("Erreur lors de la mise à jour: Ajouter /api devant toutes tes routes dans ton routeur Go.", err);
}
}

View File

@ -0,0 +1,123 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Lock, User } from "lucide-react";
export default function Login() {
const navigate = useNavigate();
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
if (!username.trim() || !password.trim()) {
setError("Veuillez remplir tous les champs");
return;
}
setIsLoading(true);
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
// Le navigateur doit inclure les cookies dans la requête et accepter le Set-Cookie
credentials: 'include',
body: JSON.stringify({
username: username,
password: password
}),
});
const data = await response.json();
if (response.ok) {
console.log('Connexion réussie:', data.message);
// Vous pouvez stocker le rôle si besoin pour l'affichage UI
localStorage.setItem('userRole', data.role);
localStorage.setItem('isLoggedIn', 'true');
navigate("/");
} else {
console.error('Erreur:', data.error);
}
} catch (error) {
console.error('Erreur réseau:', error);
}
// TODO: Implémenter l'authentification ici
console.log("Login attempt:", { username, password });
setIsLoading(false);
};
return (
<div className="min-h-screen bg-background flex items-center justify-center p-4">
<Card className="w-full max-w-md">
<CardHeader className="text-center">
<div className="mx-auto mb-4 w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center">
<Lock className="w-6 h-6 text-primary" />
</div>
<CardTitle className="text-2xl">Connexion</CardTitle>
<CardDescription>
Connectez-vous pour accéder à l'administration
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="username">Nom d'utilisateur</Label>
<div className="relative">
<User className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
id="username"
type="text"
placeholder="Entrez votre nom d'utilisateur"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="pl-10"
disabled={isLoading}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="password">Mot de passe</Label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
id="password"
type="password"
placeholder="Entrez votre mot de passe"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="pl-10"
disabled={isLoading}
/>
</div>
</div>
{error && (
<p className="text-sm text-destructive text-center">{error}</p>
)}
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? "Connexion..." : "Se connecter"}
</Button>
</form>
</CardContent>
</Card>
</div>
);
}

View File

@ -0,0 +1,210 @@
import { useState } from "react";
import { Layout } from "@/components/Layout";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { useToast } from "@/hooks/use-toast";
import { Trash2, UserPlus, Users } from "lucide-react";
interface User {
id: string;
username: string;
email: string;
}
// Mock data - à remplacer par les vrais appels API
const mockUsers: User[] = [
{ id: "1", username: "admin", email: "admin@example.com" },
{ id: "2", username: "john.doe", email: "john.doe@example.com" },
{ id: "3", username: "jane.smith", email: "jane.smith@example.com" },
];
export default function UsersPage() {
const { toast } = useToast();
const [users, setUsers] = useState<User[]>(mockUsers);
const [newUsername, setNewUsername] = useState("");
const [newEmail, setNewEmail] = useState("");
const [newPassword, setNewPassword] = useState("");
const [isAdding, setIsAdding] = useState(false);
const handleAddUser = async (e: React.FormEvent) => {
e.preventDefault();
if (!newUsername.trim() || !newEmail.trim() || !newPassword.trim()) {
toast({
title: "Erreur",
description: "Veuillez remplir tous les champs",
variant: "destructive",
});
return;
}
setIsAdding(true);
// TODO: Implémenter l'appel API ici
console.log("Add user:", { username: newUsername, email: newEmail, password: newPassword });
// Simulation d'ajout
const newUser: User = {
id: `user-${Date.now()}`,
username: newUsername.trim(),
email: newEmail.trim(),
};
setUsers([...users, newUser]);
setNewUsername("");
setNewEmail("");
setNewPassword("");
setIsAdding(false);
toast({
title: "Utilisateur ajouté",
description: `L'utilisateur ${newUser.username} a été créé avec succès`,
});
};
const handleDeleteUser = async (userId: string, username: string) => {
// TODO: Implémenter l'appel API ici
console.log("Delete user:", userId);
setUsers(users.filter(user => user.id !== userId));
toast({
title: "Utilisateur supprimé",
description: `L'utilisateur ${username} a été supprimé`,
});
};
return (
<Layout>
<div className="space-y-6">
<div>
<h1 className="text-2xl sm:text-3xl font-bold">Gestion des utilisateurs</h1>
<p className="text-muted-foreground mt-1">
Gérez les accès à l'administration
</p>
</div>
<div className="grid gap-6 lg:grid-cols-3">
{/* Formulaire d'ajout */}
<Card className="lg:col-span-1">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<UserPlus className="w-5 h-5" />
Nouvel utilisateur
</CardTitle>
<CardDescription>
Créer un nouveau compte utilisateur
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleAddUser} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="new-username">Nom d'utilisateur</Label>
<Input
id="new-username"
type="text"
placeholder="john.doe"
value={newUsername}
onChange={(e) => setNewUsername(e.target.value)}
disabled={isAdding}
/>
</div>
<div className="space-y-2">
<Label htmlFor="new-email">Adresse email</Label>
<Input
id="new-email"
type="email"
placeholder="john.doe@example.com"
value={newEmail}
onChange={(e) => setNewEmail(e.target.value)}
disabled={isAdding}
/>
</div>
<div className="space-y-2">
<Label htmlFor="new-password">Mot de passe</Label>
<Input
id="new-password"
type="password"
placeholder="••••••••"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
disabled={isAdding}
/>
</div>
<Button type="submit" className="w-full" disabled={isAdding}>
{isAdding ? "Création..." : "Créer l'utilisateur"}
</Button>
</form>
</CardContent>
</Card>
{/* Liste des utilisateurs */}
<Card className="lg:col-span-2">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Users className="w-5 h-5" />
Utilisateurs ({users.length})
</CardTitle>
<CardDescription>
Liste de tous les utilisateurs enregistrés
</CardDescription>
</CardHeader>
<CardContent>
{users.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
Aucun utilisateur enregistré
</div>
) : (
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Nom d'utilisateur</TableHead>
<TableHead className="hidden sm:table-cell">Email</TableHead>
<TableHead className="w-[80px] text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{users.map((user) => (
<TableRow key={user.id}>
<TableCell className="font-medium">
<div>
{user.username}
<div className="text-sm text-muted-foreground sm:hidden">
{user.email}
</div>
</div>
</TableCell>
<TableCell className="hidden sm:table-cell">
{user.email}
</TableCell>
<TableCell className="text-right">
<Button
variant="ghost"
size="icon"
onClick={() => handleDeleteUser(user.id, user.username)}
className="text-destructive hover:text-destructive hover:bg-destructive/10"
>
<Trash2 className="w-4 h-4" />
<span className="sr-only">Supprimer {user.username}</span>
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
</CardContent>
</Card>
</div>
</div>
</Layout>
);
}

View File

@ -26,6 +26,11 @@ export interface Node {
lastSeen: Date;
}
export interface NodeDeleteRequest {
node_id: number;
service_id: number;
}
export const STATUS_CODE_MAP: Record<number, ServiceStatus> = {
0: 'down',
1: 'operational',