Security added on delete service and list all node + cleaning some code
This commit is contained in:
@ -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>
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
17
frontend/src/components/ProtectedRoute.tsx
Normal file
17
frontend/src/components/ProtectedRoute.tsx
Normal 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;
|
||||
@ -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
20
frontend/src/lib/axios.ts
Normal 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;
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
123
frontend/src/pages/Login.tsx
Normal file
123
frontend/src/pages/Login.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
210
frontend/src/pages/Users.tsx
Normal file
210
frontend/src/pages/Users.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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',
|
||||
|
||||
Reference in New Issue
Block a user