style(i18n): translate all code and comments to English

Rename French identifiers to English across all source files:
  - validate_cidr / validate_ip_in_network (was valider_*)
  - known_protocol (was protocole_connu)
  - counter / doubled (was compteur / double)
  - InvalidCidr / InvalidIp / IpOutsideNetwork (was *Invalide / *HorsReseau)
  - test names and error messages

All comments, doc strings, .expect() messages, and tracing logs converted.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-15 19:56:47 +02:00
parent 4c11a8608b
commit 18804e740c
10 changed files with 346 additions and 354 deletions

View File

@@ -1,8 +1,8 @@
// app.rs — Composants racine de l'application Leptos
// app.rs — Root Leptos components
//
// Ce fichier contient deux composants :
// - `Shell` : le document HTML complet (head + body) — SSR uniquement
// - `App` : le contenu de la page avec le routeur — partagé SSR + WASM
// This file contains two components:
// - `Shell` : full HTML document (head + body) — SSR only
// - `App` : page content and router — shared between SSR and WASM
use leptos::prelude::*;
use leptos_meta::*;
@@ -13,89 +13,88 @@ use leptos_router::{
use crate::client::home::HomePage;
// Shell — document HTML complet rendu par le serveur Axum
// Shell — full HTML document rendered by the Axum server.
//
// Ce composant n'existe qu'en mode SSR (`#[cfg(feature = "ssr")]`).
// Il fournit la structure HTML de base que leptos_meta ne peut pas créer seul :
// un <head> et un <body> valides. Sans ça, les composants <Title>, <Stylesheet>
// de leptos_meta n'ont nulle part où s'injecter.
// This component only exists in SSR mode (`#[cfg(feature = "ssr")]`).
// It provides the HTML skeleton that leptos_meta requires:
// a valid <head> and <body>. Without it, <Title> and <Stylesheet>
// components have nowhere to inject their output.
//
// Flux de rendu SSR :
// 1. Axum appelle Shell() pour chaque requête
// 2. Shell rend le <head> avec MetaTags (placeholder rempli par App)
// 3. Shell rend le <body> contenant App()
// 4. App() appelle provide_meta_context() et définit les métadonnées
// 5. Leptos collecte les métadonnées et les injecte dans MetaTags rétroactivement
// 6. HydrationScripts génère les <script> pour charger le bundle WASM
// SSR rendering flow:
// 1. Axum calls Shell() for each incoming request
// 2. Shell renders <head> with MetaTags (a placeholder filled by App)
// 3. Shell renders <body> containing App()
// 4. App() calls provide_meta_context() and registers metadata
// 5. Leptos retroactively injects that metadata into MetaTags
// 6. HydrationScripts generates <script> tags to load the WASM bundle
#[cfg(feature = "ssr")]
#[component]
pub fn Shell(
// LeptosOptions contient la config du projet (chemins, noms de fichiers, ports...)
// Utilisée par HydrationScripts pour construire les URLs du bundle WASM.
// LeptosOptions holds project configuration (paths, file names, ports...).
// Used by HydrationScripts to build the WASM bundle URLs.
options: leptos::config::LeptosOptions,
) -> impl IntoView {
view! {
<!DOCTYPE html>
<html lang="fr">
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
// MetaTags : placeholder leptos_meta injecte les balises collectées
// depuis les composants <Title>, <Stylesheet>, <Meta>... définis dans App().
// MetaTags: placeholder where leptos_meta injects tags collected
// from <Title>, <Stylesheet>, <Meta>... defined inside App().
<MetaTags/>
// HydrationScripts : génère les balises <link> et <script> qui chargent
// le bundle WASM compilé par trunk et appellent la fonction hydrate() de lib.rs.
// HydrationScripts: generates <link> and <script> tags that load
// the trunk-compiled WASM bundle and call hydrate() from lib.rs.
<HydrationScripts options=options.clone()/>
// AutoReload : hot-reload en développement (no-op en production).
// S'active uniquement si la variable d'env LEPTOS_WATCH est définie.
// AutoReload: hot-reload during development (no-op in production).
// Only activates when the LEPTOS_WATCH environment variable is set.
<AutoReload options/>
</head>
<body>
// App() s'exécute ici, fournit le contexte meta et rend le contenu de la page
// App() runs here, provides the meta context, and renders page content.
<App/>
</body>
</html>
}
}
// App — composant racine partagé entre le serveur (SSR) et le navigateur (WASM)
// App — root component shared between the server (SSR) and the browser (WASM).
//
// Ce composant est rendu :
// - côté serveur : dans le <body> du Shell, pour générer le HTML
// - côté navigateur : via hydrate() dans lib.rs, pour attacher la réactivité
// This component is rendered:
// - server-side : inside the Shell <body>, to generate HTML
// - client-side : via hydrate() in lib.rs, to attach reactivity
//
// `-> impl IntoView` : retourne "quelque chose affichable". Le type exact est opaque
// car le compilateur Leptos génère un type complexe à partir de la macro `view!`.
// `-> impl IntoView` : returns "something displayable". The concrete type is
// opaque because Leptos's `view!` macro generates a complex internal type.
#[component]
pub fn App() -> impl IntoView {
// Initialise le système de métadonnées Leptos.
// Sans cet appel, <Title>, <Stylesheet>, <Meta> dans les composants enfants
// n'auraient pas de contexte où stocker les métadonnées.
// Initialize the Leptos metadata context.
// Without this call, <Title>, <Stylesheet>, and <Meta> in child components
// would have no context to store their metadata in.
provide_meta_context();
view! {
// Définit le titre de l'onglet navigateur
<Title text="Rust IPAM — Gestionnaire d'adresses IP"/>
<Title text="Rust IPAM — IP Address Manager"/>
// Charge le CSS global depuis /pkg/rust-ipam.css
// Ce fichier est généré par trunk à partir de style.css (si ajouté plus tard)
// Load the global CSS from /pkg/rust-ipam.css.
// This file is generated by trunk from style.css (to be added later).
<Stylesheet id="main" href="/pkg/rust-ipam.css"/>
// Le Router gère la navigation sans rechargement de page.
// Côté serveur, il détermine quel composant rendre selon l'URL.
// Router handles client-side navigation without full page reloads.
// On the server, it determines which component to render for the requested URL.
<Router>
<main>
// <Routes> est le conteneur pour toutes les définitions de routes.
// `fallback` est affiché si aucune route ne correspond.
// <Routes> is the container for all route definitions.
// `fallback` is displayed when no route matches the current URL.
<Routes fallback=|| view! {
<div class="page-erreur">
<h1>"404 — Page introuvable"</h1>
<a href="/">"Retour à l'accueil"</a>
<div class="not-found">
<h1>"404 — Page not found"</h1>
<a href="/">"Back to home"</a>
</div>
}>
// path!(/) correspond à l'URL racine "/"
// Ajouter de nouvelles pages ici :
// <Route path=path!("/reseaux") view=ReseauxPage/>
// path!(/) matches the root URL "/"
// Add new pages here, e.g.:
// <Route path=path!("/networks") view=NetworksPage/>
<Route path=path!("/") view=HomePage/>
</Routes>
</main>

View File

@@ -1,75 +1,75 @@
// client/home.rs — Page d'accueil
// client/home.rs — Home page
//
// Ce fichier illustre les concepts fondamentaux de Leptos :
// - Composants (`#[component]`)
// - Signals (valeurs réactives)
// - Memos (valeurs dérivées)
// - Gestion d'événements (`on:click`)
// Demonstrates the core Leptos concepts:
// - Components (`#[component]`)
// - Signals (reactive values)
// - Memos (derived values)
// - Event handling (`on:click`)
use leptos::prelude::*;
// Composant de la page d'accueil.
// Home page component.
//
// En Leptos, un composant est une simple fonction Rust avec l'attribut `#[component]`.
// Elle est appelée une seule fois pour construire le graphe réactif
// ce n'est pas comme React où le composant se "ré-exécute" à chaque mise à jour.
// In Leptos, a component is a plain Rust function annotated with `#[component]`.
// It is called once to build the reactive graph
// unlike React, it does not re-execute on every update.
#[component]
pub fn HomePage() -> impl IntoView {
// `RwSignal<T>` (Read-Write Signal) est une valeur réactive mutable.
// `RwSignal<T>` (Read-Write Signal) is a mutable reactive value.
//
// Quand on appelle `.set()` ou `.update()`, Leptos identifie automatiquement
// tous les éléments du DOM qui dépendent de ce signal et les met à jour
// sans Virtual DOM, sans diff complet : seulement ce qui change.
// When `.set()` or `.update()` is called, Leptos automatically identifies
// all DOM nodes that depend on this signal and updates only those
// no Virtual DOM, no full diff: surgical updates only.
//
// `i32` = entier signé 32 bits (le type entier par défaut en Rust)
let compteur = RwSignal::new(0i32);
// `i32` = signed 32-bit integer (Rust's default integer type)
let counter = RwSignal::new(0i32);
// `Memo<T>` est une valeur calculée à partir d'un ou plusieurs signals.
// Elle se recalcule automatiquement quand `compteur` change, mais
// ne notifie ses dépendants que si sa valeur a effectivement changé.
// `Memo<T>` is a value derived from one or more signals.
// It recomputes automatically when `counter` changes, but only
// notifies its dependents when the result actually differs.
//
// `move |_|` : une closure qui capture `compteur` par déplacement (ownership).
// - `move` : la closure prend possession de `compteur`
// - `|_|` : elle ignore son argument (la valeur précédente du memo)
let double = Memo::new(move |_| compteur.get() * 2);
// `move |_|`: a closure that captures `counter` by move (takes ownership).
// - `move` : the closure owns `counter`
// - `|_|` : ignores the argument (the previous memo value)
let doubled = Memo::new(move |_| counter.get() * 2);
view! {
<div class="page-accueil">
<div class="home-page">
<h1>"Rust IPAM"</h1>
<p class="sous-titre">"Gestionnaire d'adresses IP"</p>
<p class="subtitle">"IP Address Manager"</p>
// --- Démonstration de la réactivité ---
// Dans un vrai projet IPAM, on afficherait ici la liste des sous-réseaux,
// les adresses disponibles, les statistiques d'utilisation...
<section class="demo-reactive">
<h2>"Réactivité Leptos"</h2>
// --- Reactivity demo ---
// In the real IPAM app, this section will show the network list,
// available addresses, usage statistics, etc.
<section class="reactivity-demo">
<h2>"Leptos Reactivity Demo"</h2>
// `{compteur}` insère la valeur du signal directement dans le DOM.
// Leptos met à jour UNIQUEMENT ce nœud texte quand compteur change.
// Pas de re-render du composant entier : c'est granulaire et efficace.
<p>"Compteur : " {compteur}</p>
// `{counter}` inserts the signal value directly into the DOM.
// Leptos updates ONLY this text node when counter changes —
// the entire component does not re-render.
<p>"Counter: " {counter}</p>
// `{double}` : idem, mais pour le memo (valeur dérivée)
<p>"Double : " {double}</p>
// `{doubled}`: same, but for the memo (derived value)
<p>"Doubled: " {doubled}</p>
<div class="boutons">
// `on:click` attache un event listener au bouton.
<div class="buttons">
// `on:click` attaches a JavaScript event listener.
//
// `.update(|n| *n += 1)`:
// - prend une closure qui reçoit une référence mutable `&mut i32`
// - `*n` déréférence le pointeur pour modifier la valeur pointée
// - `+= 1` incrémente la valeur en place
<button on:click=move |_| compteur.update(|n| *n += 1)>
// - takes a closure receiving a `&mut i32`
// - `*n` dereferences the pointer to modify the value
// - `+= 1` increments in place
<button on:click=move |_| counter.update(|n| *n += 1)>
"+"
</button>
<button on:click=move |_| compteur.update(|n| *n -= 1)>
<button on:click=move |_| counter.update(|n| *n -= 1)>
"-"
</button>
// `.set(0)` remplace directement la valeur (plus simple qu'update ici)
<button on:click=move |_| compteur.set(0)>
"Réinitialiser"
// `.set(0)` replaces the value directly (simpler than update here)
<button on:click=move |_| counter.set(0)>
"Reset"
</button>
</div>
</section>

View File

@@ -1,12 +1,12 @@
// client/mod.rs — Module client (composants UI)
// client/mod.rs — Client UI module
//
// Contient les pages et composants Leptos de l'application.
// Contains Leptos pages and components.
//
// Important : malgré le nom "client", ce code s'exécute des DEUX côtés :
// - Côté serveur : pour générer le HTML initial (SSR)
// - Côté navigateur : compilé en WASM pour rendre l'interface interactive
// Despite the name "client", this code runs on BOTH sides:
// - Server-side : to generate the initial HTML (SSR)
// - Browser : compiled to WASM to make the interface interactive
//
// Ne pas mettre ici de code qui nécessite des APIs navigateur (window, document...)
// sans le protéger avec `#[cfg(target_arch = "wasm32")]`.
// Do not place code here that requires browser-only APIs (window, document...)
// without guarding it with `#[cfg(target_arch = "wasm32")]`.
pub mod home; // Page d'accueil
pub mod home; // Home page

View File

@@ -1,41 +1,41 @@
// lib.rs — Racine de la bibliothèque partagée
// lib.rs — Shared library root
//
// Ce fichier est compilé dans les DEUX modes :
// "ssr" → le serveur Axum l'utilise pour rendre du HTML
// "hydrate" → trunk le compile en WebAssembly pour le navigateur
// This file is compiled in BOTH modes:
// "ssr" → used by the Axum server to render HTML
// "hydrate" → compiled by trunk into WebAssembly for the browser
//
// C'est ce partage de code qui rend Leptos "full-stack" :
// on écrit les composants une fois, ils s'exécutent des deux côtés.
// This code sharing is what makes Leptos "full-stack":
// components are written once and run on both sides.
// Déclaration des sous-modules de cette bibliothèque.
// `pub` les rend accessibles depuis main.rs et d'autres crates.
pub mod app; // Composant racine App() et configuration du routeur
pub mod client; // Pages et composants de l'interface utilisateur
pub mod models; // Structs de données partagés server + client (Network, Host, Port, Application)
pub mod server; // Handlers HTTP et logique métier côté serveur
// Declare the sub-modules of this library.
// `pub` makes them accessible from main.rs and other crates.
pub mod app; // Root App() component and router configuration
pub mod client; // UI pages and Leptos components
pub mod models; // Shared data structs: Network, Host, Port, Application
pub mod server; // HTTP handlers and server-side business logic
// Point d'entrée WebAssembly — exécuté par le navigateur au chargement du bundle .wasm
// WebAssembly entry point — called by the browser when the .wasm bundle loads.
//
// `#[cfg(feature = "hydrate")]` : ce code n'existe que dans le bundle WASM.
// `#[wasm_bindgen(start)]` : demande à wasm-bindgen d'appeler cette fonction
// automatiquement sans intervention JavaScript.
// `#[cfg(feature = "hydrate")]` : this code only exists in the WASM bundle.
// `#[wasm_bindgen(start)]` : instructs wasm-bindgen to call this function
// automatically, without any JavaScript glue.
#[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen(start)]
pub fn hydrate() {
use crate::app::App;
// Active les messages d'erreur Rust dans la console du navigateur.
// Sans ça, un panic Rust en WASM affiche juste "unreachable executed" — inutile.
// `set_once()` garantit qu'on ne l'initialise pas plusieurs fois si hydrate() est appelé plusieurs fois.
// Enable Rust panic messages in the browser console.
// Without this, a Rust panic in WASM only shows "unreachable executed" — useless.
// `set_once()` ensures the hook is registered at most once.
console_error_panic_hook::set_once();
// Monte l'application Leptos dans le <body> de la page HTML.
// Mount the Leptos application into the <body> of the HTML page.
//
// En mode "hydration" (SSR + WASM), Leptos ne recrée pas le DOM depuis zéro.
// Il trouve le HTML déjà rendu par le serveur et y attache les event listeners
// pour rendre l'interface interactive. C'est plus rapide qu'un SPA classique
// qui construit tout le DOM côté client.
// In "hydration" mode (SSR + WASM), Leptos does not rebuild the DOM from scratch.
// It finds the HTML already rendered by the server and attaches event listeners
// to make the interface interactive. This is faster than a classic SPA
// that builds the entire DOM on the client side.
//
// `hydrate_body` (Leptos 0.7) = mode SSR + hydration (≠ `mount_to_body` qui repart de zéro)
// `hydrate_body` (Leptos 0.7) = SSR + hydration mode (≠ `mount_to_body` which starts fresh)
leptos::mount::hydrate_body(App);
}

View File

@@ -1,92 +1,91 @@
// main.rs — Point d'entrée du serveur Axum
// main.rs — Axum server entry point
//
// Ce fichier est compilé UNIQUEMENT en mode "ssr" (Server-Side Rendering).
// `#[cfg(feature = "ssr")]` est l'équivalent d'un `#ifdef` en C :
// le code qu'il protège n'existe pas dans le bundle WASM.
// This file is compiled ONLY when the "ssr" feature is enabled.
// `#[cfg(feature = "ssr")]` works like `#ifdef` in C:
// the guarded code does not exist in the WASM bundle.
//
// Pour lancer le serveur :
// Run the server:
// cargo run --features ssr
//
// Pour lancer avec des logs détaillés :
// Run with verbose logs:
// RUST_LOG=debug cargo run --features ssr
#[cfg(feature = "ssr")]
#[tokio::main]
// `#[tokio::main]` transforme `fn main()` synchrone en une fonction asynchrone,
// gérée par le runtime Tokio. Sans ça, Rust ne sait pas exécuter du code `async`.
// `#[tokio::main]` turns the synchronous `fn main()` into an async function
// managed by the Tokio runtime. Without it, Rust cannot execute `async` code.
async fn main() {
use axum::Router;
use leptos::config::get_configuration;
use leptos_axum::{generate_route_list, LeptosRoutes};
use leptos::view;
use leptos_axum::{generate_route_list, LeptosRoutes};
use rust_ipam::{
app::{App, Shell},
server::{config::AppConfig, routes::not_found_handler},
};
use tower_http::services::ServeDir;
// Initialise les logs structurés.
// tracing::info!(), tracing::warn!(), etc. n'affichent rien sans cet initialisateur.
// RUST_LOG=debug cargo run --features ssr → active les logs debug
// Initialize structured logging.
// tracing::info!(), tracing::warn!(), etc. produce no output without this.
// RUST_LOG=debug cargo run --features ssr → enables debug-level logs
tracing_subscriber::fmt()
.with_env_filter(
std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()),
)
.init();
tracing::info!("Démarrage du serveur Rust IPAM...");
tracing::info!("Starting Rust IPAM server...");
// Charge la configuration depuis .env / variables d'environnement.
// On arrête le serveur immédiatement si la config est invalide — il ne peut
// pas fonctionner sans savoir à quelle base de données se connecter.
// Load configuration from environment variables / .env file.
// The server cannot start without knowing which database to connect to,
// so we abort immediately on any configuration error.
let app_config = AppConfig::from_env()
.expect("Erreur de configuration — vérifier le fichier .env");
.expect("Configuration error — check your .env file");
tracing::info!("Base de données : {} ({})", app_config.backend, app_config.database_url);
tracing::info!("Database: {} ({})", app_config.backend, app_config.database_url);
// `Some("Cargo.toml")` indique à Leptos de lire la section
// [package.metadata.leptos] du Cargo.toml pour la configuration
// (noms de fichiers, chemins, adresse serveur...).
// `Some("Cargo.toml")` tells Leptos to read the [package.metadata.leptos]
// section from Cargo.toml (file paths, output names, server address...).
let conf = get_configuration(Some("Cargo.toml"))
.expect("Impossible de charger la configuration Leptos depuis Cargo.toml");
.expect("Failed to load Leptos configuration from Cargo.toml");
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
// Analyse les composants `<Route>` dans `App` pour construire
// la liste des URLs que Leptos SSR doit gérer.
// Walk all `<Route>` components inside `App` to build the list of URLs
// that Leptos SSR must handle.
let routes = generate_route_list(App);
// Construit le routeur Axum avec le pattern builder (chaînage de méthodes).
// Build the Axum router using the builder pattern (method chaining).
let app = Router::new()
// Sert les fichiers statiques compilés par trunk (WASM, JS...).
// Trunk les place dans target/site/pkg/ (configuré dans [package.metadata.leptos]).
// Serve static files compiled by trunk (WASM, JS...).
// Trunk places them in target/site/pkg/ as configured in [package.metadata.leptos].
.nest_service("/pkg", ServeDir::new("target/site/pkg"))
// Branche les routes Leptos dans Axum.
// Pour chaque URL, Axum rend Shell() en HTML et le renvoie au navigateur.
// Shell() contient App() qui fournit le contenu de la page.
// Mount all Leptos routes into Axum.
// For each URL, Axum renders Shell() to HTML and sends it to the browser.
// Shell() contains App(), which provides the page content.
.leptos_routes(&leptos_options, routes, {
// On clone les options pour les capturer dans la closure.
// Le `move` transfère la propriété de `leptos_options` dans la closure.
// Clone options so the closure can capture them.
// `move` transfers ownership of `leptos_options` into the closure.
let leptos_options = leptos_options.clone();
move || view! { <Shell options=leptos_options.clone()/> }
})
.fallback(not_found_handler)
// Partage les options Leptos avec tous les handlers via le système d'état Axum.
// Share Leptos options with all handlers via Axum's state system.
.with_state(leptos_options);
let listener = tokio::net::TcpListener::bind(&addr)
.await
.expect(&format!("Impossible d'écouter sur l'adresse {}", addr));
.expect(&format!("Failed to bind to address {}", addr));
tracing::info!("Serveur disponible sur http://{}", addr);
tracing::info!("Server listening on http://{}", addr);
axum::serve(listener, app)
.await
.expect("Erreur critique du serveur");
.expect("Fatal server error");
}
// Ce bloc vide est obligatoire pour que le compilateur trouve un `fn main()`
// en mode WASM (où la feature "ssr" n'est pas activée).
// En WASM, le vrai point d'entrée est la fonction `hydrate()` dans lib.rs.
// This empty block is required so the compiler finds a `fn main()`
// when building in WASM mode (where the "ssr" feature is not enabled).
// In WASM, the real entry point is `hydrate()` in lib.rs.
#[cfg(not(feature = "ssr"))]
fn main() {}

View File

@@ -1,93 +1,92 @@
// models.rs — Modèles de données partagés (server + client)
// models.rs — Shared data models (server + client)
//
// Ce module définit les structs qui représentent les entités métier du projet IPAM.
// Ils sont compilés côté serveur ET côté WASM car Leptos en a besoin des deux côtés :
// - Serveur : pour lire/écrire en BDD et rendre le HTML
// - Client : pour afficher les données dans les composants Leptos
// This module defines the structs that represent the IPAM domain entities.
// They are compiled for both the server and WASM, because Leptos needs them
// on both sides:
// - Server : to read/write the database and render HTML
// - Client : to display data inside Leptos components
//
// Chaque struct dérive `Serialize` et `Deserialize` de serde.
// C'est obligatoire pour que Leptos puisse transférer les données entre
// le serveur et le navigateur via les "server functions" (#[server]).
// Each struct derives `Serialize` and `Deserialize` from serde.
// This is required for Leptos to transfer data between the server and the
// browser through server functions (#[server]).
use serde::{Deserialize, Serialize};
// ─── Réseau ───────────────────────────────────────────────────────────────────
// ─── Network ──────────────────────────────────────────────────────────────────
/// Un réseau IP défini par sa plage CIDR.
/// An IP network defined by its CIDR range.
///
/// Exemple : { id: 1, cidr: "192.168.1.0/24" }
/// → plage de 192.168.1.0 à 192.168.1.255 (254 hôtes utilisables)
/// Example: { id: 1, cidr: "192.168.1.0/24" }
/// → covers 192.168.1.0 to 192.168.1.255 (254 usable hosts)
///
/// La notation CIDR (Classless Inter-Domain Routing) combine l'adresse réseau
/// et le masque en un seul champ : <adresse>/<longueur du préfixe>.
/// /24 = 24 bits de masque = 255.255.255.0
/// CIDR (Classless Inter-Domain Routing) combines the network address and
/// the subnet mask into a single field: <address>/<prefix length>.
/// /24 = 24-bit mask = 255.255.255.0
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Network {
/// Identifiant unique auto-incrémenté par la base de données.
/// `i64` est le type entier signé 64 bits — correspond à `BIGINT` en SQL.
/// Unique identifier, auto-incremented by the database.
/// `i64` is a signed 64-bit integer — maps to `BIGINT` in SQL.
pub id: i64,
/// Plage d'adresses en notation CIDR.
/// Ex: "10.0.0.0/8", "172.16.0.0/12", "192.168.1.0/24"
/// Address range in CIDR notation.
/// Examples: "10.0.0.0/8", "172.16.0.0/12", "192.168.1.0/24"
pub cidr: String,
}
// ─── Hôte ─────────────────────────────────────────────────────────────────────
// ─── Host ─────────────────────────────────────────────────────────────────────
/// Un hôte (machine, serveur, équipement réseau) appartenant à un réseau.
/// A host (server, workstation, network device) belonging to a network.
///
/// Contrainte : l'IP doit appartenir à la plage CIDR du réseau référencé
/// par `network_id`. Cette contrainte est vérifiée à la création/modification.
/// Constraint: the IP address must fall within the CIDR range of the network
/// referenced by `network_id`. This is enforced on creation and update.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Host {
pub id: i64,
/// Nom descriptif de l'hôte. Ex: "serveur-web-01", "routeur-principal"
/// Human-readable name. Examples: "web-server-01", "main-router"
pub name: String,
/// Adresse IPv4 de l'hôte, stockée en texte.
/// Ex: "192.168.1.10"
/// On utilise String plutôt que IpAddr pour simplifier la sérialisation
/// et le stockage en base de données.
/// IPv4 address stored as text. Example: "192.168.1.10"
/// We use String instead of IpAddr to simplify serialization
/// and database storage.
pub ip: String,
/// Référence vers le réseau auquel appartient cet hôte.
/// C'est une "clé étrangère" (Foreign Key) vers la table `networks`.
/// Foreign key referencing the network this host belongs to.
pub network_id: i64,
}
// ─── Port ─────────────────────────────────────────────────────────────────────
/// Un port réseau ouvert sur un hôte, avec sa description probable.
/// A network port open on a host, with its likely protocol description.
///
/// Les numéros de ports standards (01023) sont des "well-known ports".
/// Un port peut être associé à plusieurs applications (association non-stricte).
/// Well-known ports (01023) have standardized protocol assignments.
/// A port can be associated with multiple applications (non-strict relation).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Port {
/// Numéro de port TCP/UDP.
/// `u16` : entier non signé 16 bits → plage 0 à 65535.
/// C'est exactement la plage valide pour les ports réseau.
/// TCP/UDP port number.
/// `u16` is an unsigned 16-bit integer → range 0 to 65535,
/// which exactly matches the valid range for network ports.
pub number: u16,
/// Description du protocole probable sur ce port.
/// `Option<String>` : peut être absent (None) si le protocole est inconnu.
/// Ex: Some("SSH"), Some("HTTPS"), None
/// Description of the likely protocol on this port.
/// `Option<String>`: may be absent (None) when the protocol is unknown.
/// Examples: Some("SSH"), Some("HTTPS"), None
pub description: Option<String>,
/// Hôte sur lequel ce port est ouvert.
/// The host on which this port is open.
pub host_id: i64,
}
impl Port {
/// Retourne la description standard pour les ports les plus courants.
/// Utilisé pour pré-remplir la description lors de l'ajout d'un port.
/// Returns the standard description for common well-known ports.
/// Used to pre-fill the description field when adding a port.
///
/// `match` est l'équivalent Rust d'un switch/case, mais exhaustif :
/// le compilateur oblige à gérer tous les cas possibles.
pub fn protocole_connu(numero: u16) -> Option<&'static str> {
// `&'static str` : référence vers une chaîne qui vit toute la durée du programme
// (les littéraux de chaînes comme "SSH" sont stockés dans le binaire compilé)
match numero {
/// `match` is Rust's exhaustive pattern-matching construct (like switch/case,
/// but the compiler enforces that all cases are handled).
pub fn known_protocol(number: u16) -> Option<&'static str> {
// `&'static str`: a reference to a string that lives for the entire
// program lifetime (string literals are stored in the compiled binary).
match number {
21 => Some("FTP"),
22 => Some("SSH"),
23 => Some("Telnet"),
@@ -100,33 +99,33 @@ impl Port {
3306 => Some("MySQL"),
5432 => Some("PostgreSQL"),
6379 => Some("Redis"),
8080 => Some("HTTP alternatif"),
_ => None, // `_` est le pattern "tout le reste" (wildcard)
8080 => Some("HTTP (alternate)"),
_ => None, // `_` is the wildcard pattern — matches everything else
}
}
}
// ─── Application ──────────────────────────────────────────────────────────────
/// Une application qui utilise un ou plusieurs ports.
/// An application that uses one or more ports.
///
/// L'association entre application et port est non-stricte :
/// un même port peut être partagé par plusieurs applications.
/// Ex: le port 80 peut être utilisé par Nginx ET par un proxy applicatif.
/// The association between an application and a port is non-strict:
/// the same port can be shared by multiple applications.
/// Example: port 80 might be used by both Nginx and an application proxy.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Application {
pub id: i64,
/// Nom de l'application. Ex: "Nginx", "PostgreSQL", "Prometheus"
/// Application name. Examples: "Nginx", "PostgreSQL", "Prometheus"
pub name: String,
}
// ─── Association ApplicationPort ──────────────────────────────────────────
// ─── ApplicationPort ──────────────────────────────────────────────────────────
/// Lien entre une application et un port (relation many-to-many).
/// Join record linking an application to a port (many-to-many relationship).
///
/// On utilise un struct dédié plutôt qu'un Vec<Port> dans Application
/// pour correspondre directement à la table de jointure en base de données.
/// A dedicated struct is used instead of Vec<Port> inside Application
/// so it maps directly to the join table in the database.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApplicationPort {
pub application_id: i64,

View File

@@ -1,44 +1,44 @@
// server/config.rs — Configuration de l'application
// server/config.rs — Application configuration
//
// Ce module charge et valide la configuration au démarrage du serveur.
// Toute la configuration passe par des variables d'environnement,
// elles-mêmes chargées depuis un fichier `.env` via dotenvy.
// This module loads and validates configuration at server startup.
// All configuration is read from environment variables,
// which can be populated from a `.env` file via dotenvy.
//
// Principe : une seule variable suffit pour choisir la base de données.
// DATABASE_URL=sqlite://data/ipam.db → SQLite (dev, pas de serveur nécessaire)
// A single variable is enough to select the database backend:
// DATABASE_URL=sqlite://data/ipam.db → SQLite (dev, no server needed)
// DATABASE_URL=postgresql://user:pw@host/db → PostgreSQL (production)
use thiserror::Error;
// ─── Erreurs de configuration ─────────────────────────────────────────────────
// ─── Configuration errors ─────────────────────────────────────────────────────
// `#[derive(Error)]` de la crate thiserror génère automatiquement l'impl
// du trait standard `std::error::Error` — pas besoin de l'écrire à la main.
// `#[derive(Error)]` from thiserror automatically generates the impl for the
// standard `std::error::Error` trait — no need to write it by hand.
//
// `#[error("...")]` définit le message affiché par Display / println!("{}", err).
// `#[error("...")]` defines the message shown by Display / println!("{}", err).
#[derive(Debug, Error)]
pub enum ConfigError {
// `#[from]` permet de convertir automatiquement un VarError en ConfigError
// via l'opérateur `?`. Ex: std::env::var("X")? dans une fn -> Result<_, ConfigError>
#[error("Variable d'environnement manquante : {0}")]
// `#[from]` auto-converts a VarError into ConfigError via the `?` operator.
// Example: std::env::var("X")? in a fn -> Result<_, ConfigError>
#[error("Missing environment variable: {0}")]
MissingVar(#[from] std::env::VarError),
#[error("URL de base de données non reconnue : '{0}' — doit commencer par sqlite:// ou postgresql://")]
#[error("Unknown database URL '{0}' — must start with sqlite:// or postgresql://")]
UnknownBackend(String),
}
// ─── Backend de base de données ───────────────────────────────────────────────
// ─── Database backend ─────────────────────────────────────────────────────────
// `#[derive(Debug, Clone)]` génère automatiquement :
// - Debug : permet d'afficher la valeur avec {:?} dans les logs
// - Clone : permet de copier la valeur (nécessaire pour l'état Axum)
// `#[derive(Debug, Clone)]` automatically generates:
// - Debug : allows printing the value with {:?} in logs
// - Clone : allows copying the value (required for Axum state)
#[derive(Debug, Clone, PartialEq)]
pub enum DatabaseBackend {
Postgres,
Sqlite,
}
// Impl Display pour pouvoir écrire tracing::info!("Backend : {}", backend)
// Display impl allows writing: tracing::info!("Backend: {}", backend)
impl std::fmt::Display for DatabaseBackend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
@@ -48,35 +48,35 @@ impl std::fmt::Display for DatabaseBackend {
}
}
// ─── Configuration principale ─────────────────────────────────────────────────
// ─── Application configuration ────────────────────────────────────────────────
#[derive(Debug, Clone)]
pub struct AppConfig {
/// URL complète de connexion à la base de données.
/// Ex: "sqlite://data/ipam.db" ou "postgresql://user:pw@localhost/ipam"
/// Full database connection URL.
/// Examples: "sqlite://data/ipam.db" or "postgresql://user:pw@localhost/ipam"
pub database_url: String,
/// Backend détecté automatiquement depuis le préfixe de DATABASE_URL.
/// Backend detected automatically from the DATABASE_URL prefix.
pub backend: DatabaseBackend,
}
impl AppConfig {
/// Charge la configuration depuis les variables d'environnement.
/// Loads configuration from environment variables.
///
/// Ordre de priorité pour les variables :
/// 1. Variables déjà définies dans le shell (ex: export DATABASE_URL=...)
/// 2. Fichier `.env` à la racine du projet
/// Variable priority order:
/// 1. Variables already set in the shell (e.g. export DATABASE_URL=...)
/// 2. `.env` file at the project root
///
/// Retourne une `ConfigError` si DATABASE_URL est absente ou invalide.
/// Returns `ConfigError` if DATABASE_URL is missing or unrecognized.
pub fn from_env() -> Result<Self, ConfigError> {
// Charge le fichier .env s'il existe.
// `let _ =` ignore silencieusement l'erreur si .env est absent —
// c'est voulu : en production les variables sont injectées directement.
// Load the .env file if it exists.
// `let _ =` silently ignores the error when .env is absent —
// this is intentional: in production, variables are injected directly.
let _ = dotenvy::dotenv();
// `std::env::var` retourne Result<String, VarError>.
// Le `?` propage l'erreur VarError, convertie en ConfigError::MissingVar
// grâce au `#[from]` défini plus haut.
// `std::env::var` returns Result<String, VarError>.
// The `?` propagates VarError, converted to ConfigError::MissingVar
// thanks to the `#[from]` attribute above.
let database_url = std::env::var("DATABASE_URL")?;
let backend = Self::detect_backend(&database_url)?;
@@ -84,17 +84,16 @@ impl AppConfig {
Ok(Self { database_url, backend })
}
/// Déduit le backend depuis le préfixe de l'URL.
/// Infers the database backend from the URL prefix.
///
/// `&str` : référence vers une chaîne — pas de copie, juste un emprunt.
/// `&str`: a borrowed string reference — no copy, just a borrow.
fn detect_backend(url: &str) -> Result<DatabaseBackend, ConfigError> {
// `starts_with` est une méthode de &str qui vérifie le préfixe
if url.starts_with("postgresql://") || url.starts_with("postgres://") {
Ok(DatabaseBackend::Postgres)
} else if url.starts_with("sqlite://") {
Ok(DatabaseBackend::Sqlite)
} else {
// `to_string()` crée un String owned depuis un &str
// `to_string()` creates an owned String from a &str
Err(ConfigError::UnknownBackend(url.to_string()))
}
}

View File

@@ -1,15 +1,13 @@
// server/mod.rs — Module serveur
// server/mod.rs — Server-side module
//
// Contient tout le code qui s'exécute uniquement côté serveur.
// Contains all code that runs on the server only.
//
// Certains sous-modules utilisent des crates SSR-only (dotenvy, ipnetwork...)
// et sont donc protégés par `#[cfg(feature = "ssr")]` pour ne pas être
// compilés dans le bundle WASM.
// Some sub-modules depend on SSR-only crates (dotenvy, ipnetwork...)
// and are therefore gated with `#[cfg(feature = "ssr")]` to prevent
// them from being compiled into the WASM bundle.
// Accessible des deux côtés (handlers HTTP sont SSR mais le module est déclaré shared)
pub mod routes;
// Modules SSR uniquement — utilisent des crates non disponibles en WASM
#[cfg(feature = "ssr")]
pub mod config;

View File

@@ -1,26 +1,26 @@
// server/routes.rs — Handlers HTTP Axum additionnels
// server/routes.rs — Additional Axum HTTP handlers
//
// Ces handlers complètent les routes gérées par Leptos.
// Exemples d'usages futurs :
// - Endpoints API REST (/api/...)
// - Exports de fichiers (CSV, PDF...)
// - Webhooks entrants
// - Health check pour le monitoring (/health)
// These handlers complement the routes managed by Leptos.
// Intended for future use:
// - REST API endpoints (/api/...)
// - File exports (CSV, PDF...)
// - Incoming webhooks
// - Health check endpoint (/health)
//
// `#[cfg(feature = "ssr")]` protège tout ce fichier :
// Axum n'existe pas dans le bundle WASM, donc on ne compile ce code qu'en mode serveur.
// The entire file is guarded by `#[cfg(feature = "ssr")]`:
// Axum does not exist in the WASM bundle, so this code is server-only.
#[cfg(feature = "ssr")]
use axum::{http::StatusCode, response::IntoResponse};
// Handler 404 — utilisé comme fallback dans main.rs pour toute URL non reconnue.
// Fallback 404 handler — used in main.rs for any URL not matched by Leptos or Axum.
//
// `impl IntoResponse` : Axum accepte n'importe quel type qui implémente ce trait.
// Un tuple `(StatusCode, &str)` l'implémente automatiquement :
// Axum en fait une réponse HTTP 404 avec le corps "Page introuvable".
// `impl IntoResponse`: Axum accepts any type that implements this trait.
// A `(StatusCode, &str)` tuple implements it automatically:
// Axum turns it into an HTTP 404 response with the given body.
//
// `async fn` est obligatoire pour les handlers Axum, même sans opération asynchrone.
// `async fn` is required for Axum handlers, even without any async operations.
#[cfg(feature = "ssr")]
pub async fn not_found_handler() -> impl IntoResponse {
(StatusCode::NOT_FOUND, "Page introuvable")
(StatusCode::NOT_FOUND, "Not found")
}

View File

@@ -1,64 +1,62 @@
// server/validation.rs — Validation des données métier
// server/validation.rs — Business rule validation
//
// Ce module vérifie les règles métier qui ne peuvent pas être exprimées
// uniquement par les types Rust. Il s'exécute côté serveur uniquement
// car il utilise `ipnetwork` pour les calculs CIDR.
// This module enforces rules that cannot be expressed through Rust's type system
// alone. It runs server-side only because it uses `ipnetwork` for CIDR arithmetic.
use ipnetwork::IpNetwork;
use std::net::IpAddr;
use thiserror::Error;
// ─── Erreurs de validation ────────────────────────────────────────────────────
// ─── Validation errors ────────────────────────────────────────────────────────
#[derive(Debug, Error)]
pub enum ValidationError {
/// Le format CIDR est invalide (ex: "192.168.1/24" au lieu de "192.168.1.0/24")
#[error("CIDR invalide '{0}' : {1}")]
CidrInvalide(String, ipnetwork::IpNetworkError),
/// The CIDR string is malformed (e.g. "192.168.1/24" instead of "192.168.1.0/24")
#[error("Invalid CIDR '{0}': {1}")]
InvalidCidr(String, ipnetwork::IpNetworkError),
/// L'adresse IP n'a pas le bon format
#[error("Adresse IP invalide '{0}' : {1}")]
IpInvalide(String, std::net::AddrParseError),
/// The IP address string is malformed
#[error("Invalid IP address '{0}': {1}")]
InvalidIp(String, std::net::AddrParseError),
/// L'IP de l'hôte n'appartient pas à la plage du réseau
#[error("L'adresse IP {ip} n'appartient pas au réseau {cidr}")]
IpHorsReseau { ip: String, cidr: String },
/// The host IP does not fall within the network's CIDR range
#[error("IP address {ip} does not belong to network {cidr}")]
IpOutsideNetwork { ip: String, cidr: String },
}
// ─── Validation du CIDR ───────────────────────────────────────────────────────
// ─── CIDR validation ──────────────────────────────────────────────────────────
/// Vérifie qu'une chaîne est un CIDR valide.
/// Retourne le réseau parsé si valide.
/// Validates that a string is a well-formed CIDR block.
/// Returns the parsed network on success.
///
/// `&str` : on emprunte la chaîne sans en prendre possession (pas de copie).
/// `Result<T, E>` : soit une valeur T (succès), soit une erreur E (échec).
pub fn valider_cidr(cidr: &str) -> Result<IpNetwork, ValidationError> {
/// `&str`: borrowed string reference — no copy, just a borrow.
/// `Result<T, E>`: either a value T (success) or an error E (failure).
pub fn validate_cidr(cidr: &str) -> Result<IpNetwork, ValidationError> {
cidr.parse::<IpNetwork>()
// `.map_err` transforme l'erreur si `parse` échoue
// `|e|` est une closure : une fonction anonyme qui prend `e` en paramètre
.map_err(|e| ValidationError::CidrInvalide(cidr.to_string(), e))
// `.map_err` transforms the error if `parse` fails.
// `|e|` is a closure: an anonymous function that takes `e` as a parameter.
.map_err(|e| ValidationError::InvalidCidr(cidr.to_string(), e))
}
// ─── Validation de l'appartenance IP ↔ Réseau ────────────────────────────────
// ─── IP-in-network validation ─────────────────────────────────────────────────
/// Vérifie qu'une adresse IP appartient à la plage d'un réseau CIDR.
/// Verifies that an IP address belongs to a given CIDR network range.
///
/// Règle métier clé : un hôte doit toujours être dans le réseau auquel il appartient.
/// Ex : 192.168.1.10 ✓ dans 192.168.1.0/24
/// 10.0.0.1 ✗ dans 192.168.1.0/24
pub fn valider_ip_dans_reseau(ip: &str, cidr: &str) -> Result<(), ValidationError> {
// On parse le CIDR et l'IP, propageant les erreurs avec `?`
let reseau = valider_cidr(cidr)?;
/// Key business rule: a host must always reside within its assigned network.
/// Example: 192.168.1.10 ✓ in 192.168.1.0/24
/// 10.0.0.1 ✗ in 192.168.1.0/24
pub fn validate_ip_in_network(ip: &str, cidr: &str) -> Result<(), ValidationError> {
let network = validate_cidr(cidr)?;
let adresse: IpAddr = ip
let address: IpAddr = ip
.parse()
.map_err(|e| ValidationError::IpInvalide(ip.to_string(), e))?;
.map_err(|e| ValidationError::InvalidIp(ip.to_string(), e))?;
// `IpNetwork::contains` retourne true si l'IP est dans la plage
if reseau.contains(adresse) {
// `IpNetwork::contains` returns true if the address falls within the range.
if network.contains(address) {
Ok(())
} else {
Err(ValidationError::IpHorsReseau {
Err(ValidationError::IpOutsideNetwork {
ip: ip.to_string(),
cidr: cidr.to_string(),
})
@@ -67,44 +65,44 @@ pub fn valider_ip_dans_reseau(ip: &str, cidr: &str) -> Result<(), ValidationErro
// ─── Tests ────────────────────────────────────────────────────────────────────
// `#[cfg(test)]` : ce bloc n'est compilé que lors de `cargo test`
// Écrire les tests directement dans le même fichier est idiomatique en Rust.
// `#[cfg(test)]`: this block is only compiled when running `cargo test`.
// Writing tests in the same file as the code being tested is idiomatic Rust.
#[cfg(test)]
mod tests {
// `super::*` importe tout depuis le module parent (ce fichier)
// `super::*` imports everything from the parent module (this file).
use super::*;
#[test]
fn ip_dans_reseau_valide() {
// `.unwrap()` est acceptable dans les tests — si ça échoue, le test échoue
assert!(valider_ip_dans_reseau("192.168.1.10", "192.168.1.0/24").is_ok());
assert!(valider_ip_dans_reseau("10.0.0.1", "10.0.0.0/8").is_ok());
assert!(valider_ip_dans_reseau("172.16.5.100", "172.16.0.0/12").is_ok());
fn ip_within_valid_network() {
// `.unwrap()` is acceptable in tests — a failure here fails the test.
assert!(validate_ip_in_network("192.168.1.10", "192.168.1.0/24").is_ok());
assert!(validate_ip_in_network("10.0.0.1", "10.0.0.0/8").is_ok());
assert!(validate_ip_in_network("172.16.5.100", "172.16.0.0/12").is_ok());
}
#[test]
fn ip_hors_reseau() {
assert!(valider_ip_dans_reseau("10.0.0.1", "192.168.1.0/24").is_err());
assert!(valider_ip_dans_reseau("192.168.2.1", "192.168.1.0/24").is_err());
fn ip_outside_network() {
assert!(validate_ip_in_network("10.0.0.1", "192.168.1.0/24").is_err());
assert!(validate_ip_in_network("192.168.2.1", "192.168.1.0/24").is_err());
}
#[test]
fn cidr_invalide() {
assert!(valider_cidr("pas-un-cidr").is_err());
assert!(valider_cidr("192.168.1/24").is_err()); // adresse tronquée
assert!(valider_cidr("192.168.1.0/33").is_err()); // préfixe > 32
fn invalid_cidr() {
assert!(validate_cidr("not-a-cidr").is_err());
assert!(validate_cidr("192.168.1/24").is_err()); // truncated address
assert!(validate_cidr("192.168.1.0/33").is_err()); // prefix > 32
}
#[test]
fn ip_invalide() {
assert!(valider_ip_dans_reseau("999.0.0.1", "192.168.1.0/24").is_err());
assert!(valider_ip_dans_reseau("pas-une-ip", "192.168.1.0/24").is_err());
fn invalid_ip() {
assert!(validate_ip_in_network("999.0.0.1", "192.168.1.0/24").is_err());
assert!(validate_ip_in_network("not-an-ip", "192.168.1.0/24").is_err());
}
#[test]
fn protocole_connu() {
assert_eq!(crate::models::Port::protocole_connu(22), Some("SSH"));
assert_eq!(crate::models::Port::protocole_connu(443), Some("HTTPS"));
assert_eq!(crate::models::Port::protocole_connu(9999), None);
fn known_protocol() {
assert_eq!(crate::models::Port::known_protocol(22), Some("SSH"));
assert_eq!(crate::models::Port::known_protocol(443), Some("HTTPS"));
assert_eq!(crate::models::Port::known_protocol(9999), None);
}
}