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:
97
src/app.rs
97
src/app.rs
@@ -1,8 +1,8 @@
|
|||||||
// app.rs — Composants racine de l'application Leptos
|
// app.rs — Root Leptos components
|
||||||
//
|
//
|
||||||
// Ce fichier contient deux composants :
|
// This file contains two components:
|
||||||
// - `Shell` : le document HTML complet (head + body) — SSR uniquement
|
// - `Shell` : full HTML document (head + body) — SSR only
|
||||||
// - `App` : le contenu de la page avec le routeur — partagé SSR + WASM
|
// - `App` : page content and router — shared between SSR and WASM
|
||||||
|
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos_meta::*;
|
use leptos_meta::*;
|
||||||
@@ -13,89 +13,88 @@ use leptos_router::{
|
|||||||
|
|
||||||
use crate::client::home::HomePage;
|
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")]`).
|
// This component only exists in SSR mode (`#[cfg(feature = "ssr")]`).
|
||||||
// Il fournit la structure HTML de base que leptos_meta ne peut pas créer seul :
|
// It provides the HTML skeleton that leptos_meta requires:
|
||||||
// un <head> et un <body> valides. Sans ça, les composants <Title>, <Stylesheet>
|
// a valid <head> and <body>. Without it, <Title> and <Stylesheet>
|
||||||
// de leptos_meta n'ont nulle part où s'injecter.
|
// components have nowhere to inject their output.
|
||||||
//
|
//
|
||||||
// Flux de rendu SSR :
|
// SSR rendering flow:
|
||||||
// 1. Axum appelle Shell() pour chaque requête
|
// 1. Axum calls Shell() for each incoming request
|
||||||
// 2. Shell rend le <head> avec MetaTags (placeholder rempli par App)
|
// 2. Shell renders <head> with MetaTags (a placeholder filled by App)
|
||||||
// 3. Shell rend le <body> contenant App()
|
// 3. Shell renders <body> containing App()
|
||||||
// 4. App() appelle provide_meta_context() et définit les métadonnées
|
// 4. App() calls provide_meta_context() and registers metadata
|
||||||
// 5. Leptos collecte les métadonnées et les injecte dans MetaTags rétroactivement
|
// 5. Leptos retroactively injects that metadata into MetaTags
|
||||||
// 6. HydrationScripts génère les <script> pour charger le bundle WASM
|
// 6. HydrationScripts generates <script> tags to load the WASM bundle
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Shell(
|
pub fn Shell(
|
||||||
// LeptosOptions contient la config du projet (chemins, noms de fichiers, ports...)
|
// LeptosOptions holds project configuration (paths, file names, ports...).
|
||||||
// Utilisée par HydrationScripts pour construire les URLs du bundle WASM.
|
// Used by HydrationScripts to build the WASM bundle URLs.
|
||||||
options: leptos::config::LeptosOptions,
|
options: leptos::config::LeptosOptions,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
view! {
|
view! {
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="fr">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8"/>
|
<meta charset="utf-8"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||||
// MetaTags : placeholder où leptos_meta injecte les balises collectées
|
// MetaTags: placeholder where leptos_meta injects tags collected
|
||||||
// depuis les composants <Title>, <Stylesheet>, <Meta>... définis dans App().
|
// from <Title>, <Stylesheet>, <Meta>... defined inside App().
|
||||||
<MetaTags/>
|
<MetaTags/>
|
||||||
// HydrationScripts : génère les balises <link> et <script> qui chargent
|
// HydrationScripts: generates <link> and <script> tags that load
|
||||||
// le bundle WASM compilé par trunk et appellent la fonction hydrate() de lib.rs.
|
// the trunk-compiled WASM bundle and call hydrate() from lib.rs.
|
||||||
<HydrationScripts options=options.clone()/>
|
<HydrationScripts options=options.clone()/>
|
||||||
// AutoReload : hot-reload en développement (no-op en production).
|
// AutoReload: hot-reload during development (no-op in production).
|
||||||
// S'active uniquement si la variable d'env LEPTOS_WATCH est définie.
|
// Only activates when the LEPTOS_WATCH environment variable is set.
|
||||||
<AutoReload options/>
|
<AutoReload options/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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/>
|
<App/>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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 :
|
// This component is rendered:
|
||||||
// - côté serveur : dans le <body> du Shell, pour générer le HTML
|
// - server-side : inside the Shell <body>, to generate HTML
|
||||||
// - côté navigateur : via hydrate() dans lib.rs, pour attacher la réactivité
|
// - client-side : via hydrate() in lib.rs, to attach reactivity
|
||||||
//
|
//
|
||||||
// `-> impl IntoView` : retourne "quelque chose affichable". Le type exact est opaque
|
// `-> impl IntoView` : returns "something displayable". The concrete type is
|
||||||
// car le compilateur Leptos génère un type complexe à partir de la macro `view!`.
|
// opaque because Leptos's `view!` macro generates a complex internal type.
|
||||||
#[component]
|
#[component]
|
||||||
pub fn App() -> impl IntoView {
|
pub fn App() -> impl IntoView {
|
||||||
// Initialise le système de métadonnées Leptos.
|
// Initialize the Leptos metadata context.
|
||||||
// Sans cet appel, <Title>, <Stylesheet>, <Meta> dans les composants enfants
|
// Without this call, <Title>, <Stylesheet>, and <Meta> in child components
|
||||||
// n'auraient pas de contexte où stocker les métadonnées.
|
// would have no context to store their metadata in.
|
||||||
provide_meta_context();
|
provide_meta_context();
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
// Définit le titre de l'onglet navigateur
|
<Title text="Rust IPAM — IP Address Manager"/>
|
||||||
<Title text="Rust IPAM — Gestionnaire d'adresses IP"/>
|
|
||||||
|
|
||||||
// Charge le CSS global depuis /pkg/rust-ipam.css
|
// Load the global CSS from /pkg/rust-ipam.css.
|
||||||
// Ce fichier est généré par trunk à partir de style.css (si ajouté plus tard)
|
// This file is generated by trunk from style.css (to be added later).
|
||||||
<Stylesheet id="main" href="/pkg/rust-ipam.css"/>
|
<Stylesheet id="main" href="/pkg/rust-ipam.css"/>
|
||||||
|
|
||||||
// Le Router gère la navigation sans rechargement de page.
|
// Router handles client-side navigation without full page reloads.
|
||||||
// Côté serveur, il détermine quel composant rendre selon l'URL.
|
// On the server, it determines which component to render for the requested URL.
|
||||||
<Router>
|
<Router>
|
||||||
<main>
|
<main>
|
||||||
// <Routes> est le conteneur pour toutes les définitions de routes.
|
// <Routes> is the container for all route definitions.
|
||||||
// `fallback` est affiché si aucune route ne correspond.
|
// `fallback` is displayed when no route matches the current URL.
|
||||||
<Routes fallback=|| view! {
|
<Routes fallback=|| view! {
|
||||||
<div class="page-erreur">
|
<div class="not-found">
|
||||||
<h1>"404 — Page introuvable"</h1>
|
<h1>"404 — Page not found"</h1>
|
||||||
<a href="/">"← Retour à l'accueil"</a>
|
<a href="/">"← Back to home"</a>
|
||||||
</div>
|
</div>
|
||||||
}>
|
}>
|
||||||
// path!(/) correspond à l'URL racine "/"
|
// path!(/) matches the root URL "/"
|
||||||
// Ajouter de nouvelles pages ici :
|
// Add new pages here, e.g.:
|
||||||
// <Route path=path!("/reseaux") view=ReseauxPage/>
|
// <Route path=path!("/networks") view=NetworksPage/>
|
||||||
<Route path=path!("/") view=HomePage/>
|
<Route path=path!("/") view=HomePage/>
|
||||||
</Routes>
|
</Routes>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1,75 +1,75 @@
|
|||||||
// client/home.rs — Page d'accueil
|
// client/home.rs — Home page
|
||||||
//
|
//
|
||||||
// Ce fichier illustre les concepts fondamentaux de Leptos :
|
// Demonstrates the core Leptos concepts:
|
||||||
// - Composants (`#[component]`)
|
// - Components (`#[component]`)
|
||||||
// - Signals (valeurs réactives)
|
// - Signals (reactive values)
|
||||||
// - Memos (valeurs dérivées)
|
// - Memos (derived values)
|
||||||
// - Gestion d'événements (`on:click`)
|
// - Event handling (`on:click`)
|
||||||
|
|
||||||
use leptos::prelude::*;
|
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]`.
|
// In Leptos, a component is a plain Rust function annotated with `#[component]`.
|
||||||
// Elle est appelée une seule fois pour construire le graphe réactif —
|
// It is called once to build the reactive graph —
|
||||||
// ce n'est pas comme React où le composant se "ré-exécute" à chaque mise à jour.
|
// unlike React, it does not re-execute on every update.
|
||||||
#[component]
|
#[component]
|
||||||
pub fn HomePage() -> impl IntoView {
|
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
|
// When `.set()` or `.update()` is called, Leptos automatically identifies
|
||||||
// tous les éléments du DOM qui dépendent de ce signal et les met à jour —
|
// all DOM nodes that depend on this signal and updates only those —
|
||||||
// sans Virtual DOM, sans diff complet : seulement ce qui change.
|
// no Virtual DOM, no full diff: surgical updates only.
|
||||||
//
|
//
|
||||||
// `i32` = entier signé 32 bits (le type entier par défaut en Rust)
|
// `i32` = signed 32-bit integer (Rust's default integer type)
|
||||||
let compteur = RwSignal::new(0i32);
|
let counter = RwSignal::new(0i32);
|
||||||
|
|
||||||
// `Memo<T>` est une valeur calculée à partir d'un ou plusieurs signals.
|
// `Memo<T>` is a value derived from one or more signals.
|
||||||
// Elle se recalcule automatiquement quand `compteur` change, mais
|
// It recomputes automatically when `counter` changes, but only
|
||||||
// ne notifie ses dépendants que si sa valeur a effectivement changé.
|
// notifies its dependents when the result actually differs.
|
||||||
//
|
//
|
||||||
// `move |_|` : une closure qui capture `compteur` par déplacement (ownership).
|
// `move |_|`: a closure that captures `counter` by move (takes ownership).
|
||||||
// - `move` : la closure prend possession de `compteur`
|
// - `move` : the closure owns `counter`
|
||||||
// - `|_|` : elle ignore son argument (la valeur précédente du memo)
|
// - `|_|` : ignores the argument (the previous memo value)
|
||||||
let double = Memo::new(move |_| compteur.get() * 2);
|
let doubled = Memo::new(move |_| counter.get() * 2);
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class="page-accueil">
|
<div class="home-page">
|
||||||
<h1>"Rust IPAM"</h1>
|
<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é ---
|
// --- Reactivity demo ---
|
||||||
// Dans un vrai projet IPAM, on afficherait ici la liste des sous-réseaux,
|
// In the real IPAM app, this section will show the network list,
|
||||||
// les adresses disponibles, les statistiques d'utilisation...
|
// available addresses, usage statistics, etc.
|
||||||
<section class="demo-reactive">
|
<section class="reactivity-demo">
|
||||||
<h2>"Réactivité Leptos"</h2>
|
<h2>"Leptos Reactivity Demo"</h2>
|
||||||
|
|
||||||
// `{compteur}` insère la valeur du signal directement dans le DOM.
|
// `{counter}` inserts the signal value directly into the DOM.
|
||||||
// Leptos met à jour UNIQUEMENT ce nœud texte quand compteur change.
|
// Leptos updates ONLY this text node when counter changes —
|
||||||
// Pas de re-render du composant entier : c'est granulaire et efficace.
|
// the entire component does not re-render.
|
||||||
<p>"Compteur : " {compteur}</p>
|
<p>"Counter: " {counter}</p>
|
||||||
|
|
||||||
// `{double}` : idem, mais pour le memo (valeur dérivée)
|
// `{doubled}`: same, but for the memo (derived value)
|
||||||
<p>"Double : " {double}</p>
|
<p>"Doubled: " {doubled}</p>
|
||||||
|
|
||||||
<div class="boutons">
|
<div class="buttons">
|
||||||
// `on:click` attache un event listener au bouton.
|
// `on:click` attaches a JavaScript event listener.
|
||||||
//
|
//
|
||||||
// `.update(|n| *n += 1)` :
|
// `.update(|n| *n += 1)`:
|
||||||
// - prend une closure qui reçoit une référence mutable `&mut i32`
|
// - takes a closure receiving a `&mut i32`
|
||||||
// - `*n` déréférence le pointeur pour modifier la valeur pointée
|
// - `*n` dereferences the pointer to modify the value
|
||||||
// - `+= 1` incrémente la valeur en place
|
// - `+= 1` increments in place
|
||||||
<button on:click=move |_| compteur.update(|n| *n += 1)>
|
<button on:click=move |_| counter.update(|n| *n += 1)>
|
||||||
"+"
|
"+"
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button on:click=move |_| compteur.update(|n| *n -= 1)>
|
<button on:click=move |_| counter.update(|n| *n -= 1)>
|
||||||
"-"
|
"-"
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
// `.set(0)` remplace directement la valeur (plus simple qu'update ici)
|
// `.set(0)` replaces the value directly (simpler than update here)
|
||||||
<button on:click=move |_| compteur.set(0)>
|
<button on:click=move |_| counter.set(0)>
|
||||||
"Réinitialiser"
|
"Reset"
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -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 :
|
// Despite the name "client", this code runs on BOTH sides:
|
||||||
// - Côté serveur : pour générer le HTML initial (SSR)
|
// - Server-side : to generate the initial HTML (SSR)
|
||||||
// - Côté navigateur : compilé en WASM pour rendre l'interface interactive
|
// - Browser : compiled to WASM to make the interface interactive
|
||||||
//
|
//
|
||||||
// Ne pas mettre ici de code qui nécessite des APIs navigateur (window, document...)
|
// Do not place code here that requires browser-only APIs (window, document...)
|
||||||
// sans le protéger avec `#[cfg(target_arch = "wasm32")]`.
|
// without guarding it with `#[cfg(target_arch = "wasm32")]`.
|
||||||
|
|
||||||
pub mod home; // Page d'accueil
|
pub mod home; // Home page
|
||||||
|
|||||||
50
src/lib.rs
50
src/lib.rs
@@ -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 :
|
// This file is compiled in BOTH modes:
|
||||||
// "ssr" → le serveur Axum l'utilise pour rendre du HTML
|
// "ssr" → used by the Axum server to render HTML
|
||||||
// "hydrate" → trunk le compile en WebAssembly pour le navigateur
|
// "hydrate" → compiled by trunk into WebAssembly for the browser
|
||||||
//
|
//
|
||||||
// C'est ce partage de code qui rend Leptos "full-stack" :
|
// This code sharing is what makes Leptos "full-stack":
|
||||||
// on écrit les composants une fois, ils s'exécutent des deux côtés.
|
// components are written once and run on both sides.
|
||||||
|
|
||||||
// Déclaration des sous-modules de cette bibliothèque.
|
// Declare the sub-modules of this library.
|
||||||
// `pub` les rend accessibles depuis main.rs et d'autres crates.
|
// `pub` makes them accessible from main.rs and other crates.
|
||||||
pub mod app; // Composant racine App() et configuration du routeur
|
pub mod app; // Root App() component and router configuration
|
||||||
pub mod client; // Pages et composants de l'interface utilisateur
|
pub mod client; // UI pages and Leptos components
|
||||||
pub mod models; // Structs de données partagés server + client (Network, Host, Port, Application)
|
pub mod models; // Shared data structs: Network, Host, Port, Application
|
||||||
pub mod server; // Handlers HTTP et logique métier côté serveur
|
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.
|
// `#[cfg(feature = "hydrate")]` : this code only exists in the WASM bundle.
|
||||||
// `#[wasm_bindgen(start)]` : demande à wasm-bindgen d'appeler cette fonction
|
// `#[wasm_bindgen(start)]` : instructs wasm-bindgen to call this function
|
||||||
// automatiquement sans intervention JavaScript.
|
// automatically, without any JavaScript glue.
|
||||||
#[cfg(feature = "hydrate")]
|
#[cfg(feature = "hydrate")]
|
||||||
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
#[wasm_bindgen::prelude::wasm_bindgen(start)]
|
||||||
pub fn hydrate() {
|
pub fn hydrate() {
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
|
|
||||||
// Active les messages d'erreur Rust dans la console du navigateur.
|
// Enable Rust panic messages in the browser console.
|
||||||
// Sans ça, un panic Rust en WASM affiche juste "unreachable executed" — inutile.
|
// Without this, a Rust panic in WASM only shows "unreachable executed" — useless.
|
||||||
// `set_once()` garantit qu'on ne l'initialise pas plusieurs fois si hydrate() est appelé plusieurs fois.
|
// `set_once()` ensures the hook is registered at most once.
|
||||||
console_error_panic_hook::set_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.
|
// In "hydration" mode (SSR + WASM), Leptos does not rebuild the DOM from scratch.
|
||||||
// Il trouve le HTML déjà rendu par le serveur et y attache les event listeners
|
// It finds the HTML already rendered by the server and attaches event listeners
|
||||||
// pour rendre l'interface interactive. C'est plus rapide qu'un SPA classique
|
// to make the interface interactive. This is faster than a classic SPA
|
||||||
// qui construit tout le DOM côté client.
|
// 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);
|
leptos::mount::hydrate_body(App);
|
||||||
}
|
}
|
||||||
|
|||||||
77
src/main.rs
77
src/main.rs
@@ -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).
|
// This file is compiled ONLY when the "ssr" feature is enabled.
|
||||||
// `#[cfg(feature = "ssr")]` est l'équivalent d'un `#ifdef` en C :
|
// `#[cfg(feature = "ssr")]` works like `#ifdef` in C:
|
||||||
// le code qu'il protège n'existe pas dans le bundle WASM.
|
// the guarded code does not exist in the WASM bundle.
|
||||||
//
|
//
|
||||||
// Pour lancer le serveur :
|
// Run the server:
|
||||||
// cargo run --features ssr
|
// cargo run --features ssr
|
||||||
//
|
//
|
||||||
// Pour lancer avec des logs détaillés :
|
// Run with verbose logs:
|
||||||
// RUST_LOG=debug cargo run --features ssr
|
// RUST_LOG=debug cargo run --features ssr
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
// `#[tokio::main]` transforme `fn main()` synchrone en une fonction asynchrone,
|
// `#[tokio::main]` turns the synchronous `fn main()` into an async function
|
||||||
// gérée par le runtime Tokio. Sans ça, Rust ne sait pas exécuter du code `async`.
|
// managed by the Tokio runtime. Without it, Rust cannot execute `async` code.
|
||||||
async fn main() {
|
async fn main() {
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
use leptos::config::get_configuration;
|
use leptos::config::get_configuration;
|
||||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
|
||||||
use leptos::view;
|
use leptos::view;
|
||||||
|
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||||
use rust_ipam::{
|
use rust_ipam::{
|
||||||
app::{App, Shell},
|
app::{App, Shell},
|
||||||
server::{config::AppConfig, routes::not_found_handler},
|
server::{config::AppConfig, routes::not_found_handler},
|
||||||
};
|
};
|
||||||
use tower_http::services::ServeDir;
|
use tower_http::services::ServeDir;
|
||||||
|
|
||||||
// Initialise les logs structurés.
|
// Initialize structured logging.
|
||||||
// tracing::info!(), tracing::warn!(), etc. n'affichent rien sans cet initialisateur.
|
// tracing::info!(), tracing::warn!(), etc. produce no output without this.
|
||||||
// RUST_LOG=debug cargo run --features ssr → active les logs debug
|
// RUST_LOG=debug cargo run --features ssr → enables debug-level logs
|
||||||
tracing_subscriber::fmt()
|
tracing_subscriber::fmt()
|
||||||
.with_env_filter(
|
.with_env_filter(
|
||||||
std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()),
|
std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()),
|
||||||
)
|
)
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
tracing::info!("Démarrage du serveur Rust IPAM...");
|
tracing::info!("Starting Rust IPAM server...");
|
||||||
|
|
||||||
// Charge la configuration depuis .env / variables d'environnement.
|
// Load configuration from environment variables / .env file.
|
||||||
// On arrête le serveur immédiatement si la config est invalide — il ne peut
|
// The server cannot start without knowing which database to connect to,
|
||||||
// pas fonctionner sans savoir à quelle base de données se connecter.
|
// so we abort immediately on any configuration error.
|
||||||
let app_config = AppConfig::from_env()
|
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
|
// `Some("Cargo.toml")` tells Leptos to read the [package.metadata.leptos]
|
||||||
// [package.metadata.leptos] du Cargo.toml pour la configuration
|
// section from Cargo.toml (file paths, output names, server address...).
|
||||||
// (noms de fichiers, chemins, adresse serveur...).
|
|
||||||
let conf = get_configuration(Some("Cargo.toml"))
|
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 leptos_options = conf.leptos_options;
|
||||||
let addr = leptos_options.site_addr;
|
let addr = leptos_options.site_addr;
|
||||||
|
|
||||||
// Analyse les composants `<Route>` dans `App` pour construire
|
// Walk all `<Route>` components inside `App` to build the list of URLs
|
||||||
// la liste des URLs que Leptos SSR doit gérer.
|
// that Leptos SSR must handle.
|
||||||
let routes = generate_route_list(App);
|
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()
|
let app = Router::new()
|
||||||
// Sert les fichiers statiques compilés par trunk (WASM, JS...).
|
// Serve static files compiled by trunk (WASM, JS...).
|
||||||
// Trunk les place dans target/site/pkg/ (configuré dans [package.metadata.leptos]).
|
// Trunk places them in target/site/pkg/ as configured in [package.metadata.leptos].
|
||||||
.nest_service("/pkg", ServeDir::new("target/site/pkg"))
|
.nest_service("/pkg", ServeDir::new("target/site/pkg"))
|
||||||
// Branche les routes Leptos dans Axum.
|
// Mount all Leptos routes into Axum.
|
||||||
// Pour chaque URL, Axum rend Shell() en HTML et le renvoie au navigateur.
|
// For each URL, Axum renders Shell() to HTML and sends it to the browser.
|
||||||
// Shell() contient App() qui fournit le contenu de la page.
|
// Shell() contains App(), which provides the page content.
|
||||||
.leptos_routes(&leptos_options, routes, {
|
.leptos_routes(&leptos_options, routes, {
|
||||||
// On clone les options pour les capturer dans la closure.
|
// Clone options so the closure can capture them.
|
||||||
// Le `move` transfère la propriété de `leptos_options` dans la closure.
|
// `move` transfers ownership of `leptos_options` into the closure.
|
||||||
let leptos_options = leptos_options.clone();
|
let leptos_options = leptos_options.clone();
|
||||||
move || view! { <Shell options=leptos_options.clone()/> }
|
move || view! { <Shell options=leptos_options.clone()/> }
|
||||||
})
|
})
|
||||||
.fallback(not_found_handler)
|
.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);
|
.with_state(leptos_options);
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind(&addr)
|
let listener = tokio::net::TcpListener::bind(&addr)
|
||||||
.await
|
.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)
|
axum::serve(listener, app)
|
||||||
.await
|
.await
|
||||||
.expect("Erreur critique du serveur");
|
.expect("Fatal server error");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ce bloc vide est obligatoire pour que le compilateur trouve un `fn main()`
|
// This empty block is required so the compiler finds a `fn main()`
|
||||||
// en mode WASM (où la feature "ssr" n'est pas activée).
|
// when building in WASM mode (where the "ssr" feature is not enabled).
|
||||||
// En WASM, le vrai point d'entrée est la fonction `hydrate()` dans lib.rs.
|
// In WASM, the real entry point is `hydrate()` in lib.rs.
|
||||||
#[cfg(not(feature = "ssr"))]
|
#[cfg(not(feature = "ssr"))]
|
||||||
fn main() {}
|
fn main() {}
|
||||||
|
|||||||
117
src/models.rs
117
src/models.rs
@@ -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.
|
// This module defines the structs that represent the IPAM domain entities.
|
||||||
// Ils sont compilés côté serveur ET côté WASM car Leptos en a besoin des deux côtés :
|
// They are compiled for both the server and WASM, because Leptos needs them
|
||||||
// - Serveur : pour lire/écrire en BDD et rendre le HTML
|
// on both sides:
|
||||||
// - Client : pour afficher les données dans les composants Leptos
|
// - 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.
|
// Each struct derives `Serialize` and `Deserialize` from serde.
|
||||||
// C'est obligatoire pour que Leptos puisse transférer les données entre
|
// This is required for Leptos to transfer data between the server and the
|
||||||
// le serveur et le navigateur via les "server functions" (#[server]).
|
// browser through server functions (#[server]).
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
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" }
|
/// Example: { id: 1, cidr: "192.168.1.0/24" }
|
||||||
/// → plage de 192.168.1.0 à 192.168.1.255 (254 hôtes utilisables)
|
/// → 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
|
/// CIDR (Classless Inter-Domain Routing) combines the network address and
|
||||||
/// et le masque en un seul champ : <adresse>/<longueur du préfixe>.
|
/// the subnet mask into a single field: <address>/<prefix length>.
|
||||||
/// /24 = 24 bits de masque = 255.255.255.0
|
/// /24 = 24-bit mask = 255.255.255.0
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Network {
|
pub struct Network {
|
||||||
/// Identifiant unique auto-incrémenté par la base de données.
|
/// Unique identifier, auto-incremented by the database.
|
||||||
/// `i64` est le type entier signé 64 bits — correspond à `BIGINT` en SQL.
|
/// `i64` is a signed 64-bit integer — maps to `BIGINT` in SQL.
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
|
|
||||||
/// Plage d'adresses en notation CIDR.
|
/// Address range in CIDR notation.
|
||||||
/// Ex: "10.0.0.0/8", "172.16.0.0/12", "192.168.1.0/24"
|
/// Examples: "10.0.0.0/8", "172.16.0.0/12", "192.168.1.0/24"
|
||||||
pub cidr: String,
|
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é
|
/// Constraint: the IP address must fall within the CIDR range of the network
|
||||||
/// par `network_id`. Cette contrainte est vérifiée à la création/modification.
|
/// referenced by `network_id`. This is enforced on creation and update.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Host {
|
pub struct Host {
|
||||||
pub id: i64,
|
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,
|
pub name: String,
|
||||||
|
|
||||||
/// Adresse IPv4 de l'hôte, stockée en texte.
|
/// IPv4 address stored as text. Example: "192.168.1.10"
|
||||||
/// Ex: "192.168.1.10"
|
/// We use String instead of IpAddr to simplify serialization
|
||||||
/// On utilise String plutôt que IpAddr pour simplifier la sérialisation
|
/// and database storage.
|
||||||
/// et le stockage en base de données.
|
|
||||||
pub ip: String,
|
pub ip: String,
|
||||||
|
|
||||||
/// Référence vers le réseau auquel appartient cet hôte.
|
/// Foreign key referencing the network this host belongs to.
|
||||||
/// C'est une "clé étrangère" (Foreign Key) vers la table `networks`.
|
|
||||||
pub network_id: i64,
|
pub network_id: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Port ─────────────────────────────────────────────────────────────────────
|
// ─── 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 (0–1023) sont des "well-known ports".
|
/// Well-known ports (0–1023) have standardized protocol assignments.
|
||||||
/// Un port peut être associé à plusieurs applications (association non-stricte).
|
/// A port can be associated with multiple applications (non-strict relation).
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Port {
|
pub struct Port {
|
||||||
/// Numéro de port TCP/UDP.
|
/// TCP/UDP port number.
|
||||||
/// `u16` : entier non signé 16 bits → plage 0 à 65535.
|
/// `u16` is an unsigned 16-bit integer → range 0 to 65535,
|
||||||
/// C'est exactement la plage valide pour les ports réseau.
|
/// which exactly matches the valid range for network ports.
|
||||||
pub number: u16,
|
pub number: u16,
|
||||||
|
|
||||||
/// Description du protocole probable sur ce port.
|
/// Description of the likely protocol on this port.
|
||||||
/// `Option<String>` : peut être absent (None) si le protocole est inconnu.
|
/// `Option<String>`: may be absent (None) when the protocol is unknown.
|
||||||
/// Ex: Some("SSH"), Some("HTTPS"), None
|
/// Examples: Some("SSH"), Some("HTTPS"), None
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
|
|
||||||
/// Hôte sur lequel ce port est ouvert.
|
/// The host on which this port is open.
|
||||||
pub host_id: i64,
|
pub host_id: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Port {
|
impl Port {
|
||||||
/// Retourne la description standard pour les ports les plus courants.
|
/// Returns the standard description for common well-known ports.
|
||||||
/// Utilisé pour pré-remplir la description lors de l'ajout d'un port.
|
/// Used to pre-fill the description field when adding a port.
|
||||||
///
|
///
|
||||||
/// `match` est l'équivalent Rust d'un switch/case, mais exhaustif :
|
/// `match` is Rust's exhaustive pattern-matching construct (like switch/case,
|
||||||
/// le compilateur oblige à gérer tous les cas possibles.
|
/// but the compiler enforces that all cases are handled).
|
||||||
pub fn protocole_connu(numero: u16) -> Option<&'static str> {
|
pub fn known_protocol(number: u16) -> Option<&'static str> {
|
||||||
// `&'static str` : référence vers une chaîne qui vit toute la durée du programme
|
// `&'static str`: a reference to a string that lives for the entire
|
||||||
// (les littéraux de chaînes comme "SSH" sont stockés dans le binaire compilé)
|
// program lifetime (string literals are stored in the compiled binary).
|
||||||
match numero {
|
match number {
|
||||||
21 => Some("FTP"),
|
21 => Some("FTP"),
|
||||||
22 => Some("SSH"),
|
22 => Some("SSH"),
|
||||||
23 => Some("Telnet"),
|
23 => Some("Telnet"),
|
||||||
@@ -100,33 +99,33 @@ impl Port {
|
|||||||
3306 => Some("MySQL"),
|
3306 => Some("MySQL"),
|
||||||
5432 => Some("PostgreSQL"),
|
5432 => Some("PostgreSQL"),
|
||||||
6379 => Some("Redis"),
|
6379 => Some("Redis"),
|
||||||
8080 => Some("HTTP alternatif"),
|
8080 => Some("HTTP (alternate)"),
|
||||||
_ => None, // `_` est le pattern "tout le reste" (wildcard)
|
_ => None, // `_` is the wildcard pattern — matches everything else
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Application ──────────────────────────────────────────────────────────────
|
// ─── 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 :
|
/// The association between an application and a port is non-strict:
|
||||||
/// un même port peut être partagé par plusieurs applications.
|
/// the same port can be shared by multiple applications.
|
||||||
/// Ex: le port 80 peut être utilisé par Nginx ET par un proxy applicatif.
|
/// Example: port 80 might be used by both Nginx and an application proxy.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Application {
|
pub struct Application {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
|
|
||||||
/// Nom de l'application. Ex: "Nginx", "PostgreSQL", "Prometheus"
|
/// Application name. Examples: "Nginx", "PostgreSQL", "Prometheus"
|
||||||
pub name: String,
|
pub name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Association Application ↔ Port ──────────────────────────────────────────
|
// ─── 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
|
/// A dedicated struct is used instead of Vec<Port> inside Application
|
||||||
/// pour correspondre directement à la table de jointure en base de données.
|
/// so it maps directly to the join table in the database.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ApplicationPort {
|
pub struct ApplicationPort {
|
||||||
pub application_id: i64,
|
pub application_id: i64,
|
||||||
|
|||||||
@@ -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.
|
// This module loads and validates configuration at server startup.
|
||||||
// Toute la configuration passe par des variables d'environnement,
|
// All configuration is read from environment variables,
|
||||||
// elles-mêmes chargées depuis un fichier `.env` via dotenvy.
|
// which can be populated from a `.env` file via dotenvy.
|
||||||
//
|
//
|
||||||
// Principe : une seule variable suffit pour choisir la base de données.
|
// A single variable is enough to select the database backend:
|
||||||
// DATABASE_URL=sqlite://data/ipam.db → SQLite (dev, pas de serveur nécessaire)
|
// DATABASE_URL=sqlite://data/ipam.db → SQLite (dev, no server needed)
|
||||||
// DATABASE_URL=postgresql://user:pw@host/db → PostgreSQL (production)
|
// DATABASE_URL=postgresql://user:pw@host/db → PostgreSQL (production)
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
// ─── Erreurs de configuration ─────────────────────────────────────────────────
|
// ─── Configuration errors ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
// `#[derive(Error)]` de la crate thiserror génère automatiquement l'impl
|
// `#[derive(Error)]` from thiserror automatically generates the impl for the
|
||||||
// du trait standard `std::error::Error` — pas besoin de l'écrire à la main.
|
// 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)]
|
#[derive(Debug, Error)]
|
||||||
pub enum ConfigError {
|
pub enum ConfigError {
|
||||||
// `#[from]` permet de convertir automatiquement un VarError en ConfigError
|
// `#[from]` auto-converts a VarError into ConfigError via the `?` operator.
|
||||||
// via l'opérateur `?`. Ex: std::env::var("X")? dans une fn -> Result<_, ConfigError>
|
// Example: std::env::var("X")? in a fn -> Result<_, ConfigError>
|
||||||
#[error("Variable d'environnement manquante : {0}")]
|
#[error("Missing environment variable: {0}")]
|
||||||
MissingVar(#[from] std::env::VarError),
|
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),
|
UnknownBackend(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Backend de base de données ───────────────────────────────────────────────
|
// ─── Database backend ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// `#[derive(Debug, Clone)]` génère automatiquement :
|
// `#[derive(Debug, Clone)]` automatically generates:
|
||||||
// - Debug : permet d'afficher la valeur avec {:?} dans les logs
|
// - Debug : allows printing the value with {:?} in logs
|
||||||
// - Clone : permet de copier la valeur (nécessaire pour l'état Axum)
|
// - Clone : allows copying the value (required for Axum state)
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum DatabaseBackend {
|
pub enum DatabaseBackend {
|
||||||
Postgres,
|
Postgres,
|
||||||
Sqlite,
|
Sqlite,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Impl Display pour pouvoir écrire tracing::info!("Backend : {}", backend)
|
// Display impl allows writing: tracing::info!("Backend: {}", backend)
|
||||||
impl std::fmt::Display for DatabaseBackend {
|
impl std::fmt::Display for DatabaseBackend {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
@@ -48,35 +48,35 @@ impl std::fmt::Display for DatabaseBackend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Configuration principale ─────────────────────────────────────────────────
|
// ─── Application configuration ────────────────────────────────────────────────
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct AppConfig {
|
pub struct AppConfig {
|
||||||
/// URL complète de connexion à la base de données.
|
/// Full database connection URL.
|
||||||
/// Ex: "sqlite://data/ipam.db" ou "postgresql://user:pw@localhost/ipam"
|
/// Examples: "sqlite://data/ipam.db" or "postgresql://user:pw@localhost/ipam"
|
||||||
pub database_url: String,
|
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,
|
pub backend: DatabaseBackend,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppConfig {
|
impl AppConfig {
|
||||||
/// Charge la configuration depuis les variables d'environnement.
|
/// Loads configuration from environment variables.
|
||||||
///
|
///
|
||||||
/// Ordre de priorité pour les variables :
|
/// Variable priority order:
|
||||||
/// 1. Variables déjà définies dans le shell (ex: export DATABASE_URL=...)
|
/// 1. Variables already set in the shell (e.g. export DATABASE_URL=...)
|
||||||
/// 2. Fichier `.env` à la racine du projet
|
/// 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> {
|
pub fn from_env() -> Result<Self, ConfigError> {
|
||||||
// Charge le fichier .env s'il existe.
|
// Load the .env file if it exists.
|
||||||
// `let _ =` ignore silencieusement l'erreur si .env est absent —
|
// `let _ =` silently ignores the error when .env is absent —
|
||||||
// c'est voulu : en production les variables sont injectées directement.
|
// this is intentional: in production, variables are injected directly.
|
||||||
let _ = dotenvy::dotenv();
|
let _ = dotenvy::dotenv();
|
||||||
|
|
||||||
// `std::env::var` retourne Result<String, VarError>.
|
// `std::env::var` returns Result<String, VarError>.
|
||||||
// Le `?` propage l'erreur VarError, convertie en ConfigError::MissingVar
|
// The `?` propagates VarError, converted to ConfigError::MissingVar
|
||||||
// grâce au `#[from]` défini plus haut.
|
// thanks to the `#[from]` attribute above.
|
||||||
let database_url = std::env::var("DATABASE_URL")?;
|
let database_url = std::env::var("DATABASE_URL")?;
|
||||||
|
|
||||||
let backend = Self::detect_backend(&database_url)?;
|
let backend = Self::detect_backend(&database_url)?;
|
||||||
@@ -84,17 +84,16 @@ impl AppConfig {
|
|||||||
Ok(Self { database_url, backend })
|
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> {
|
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://") {
|
if url.starts_with("postgresql://") || url.starts_with("postgres://") {
|
||||||
Ok(DatabaseBackend::Postgres)
|
Ok(DatabaseBackend::Postgres)
|
||||||
} else if url.starts_with("sqlite://") {
|
} else if url.starts_with("sqlite://") {
|
||||||
Ok(DatabaseBackend::Sqlite)
|
Ok(DatabaseBackend::Sqlite)
|
||||||
} else {
|
} 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()))
|
Err(ConfigError::UnknownBackend(url.to_string()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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...)
|
// Some sub-modules depend on SSR-only crates (dotenvy, ipnetwork...)
|
||||||
// et sont donc protégés par `#[cfg(feature = "ssr")]` pour ne pas être
|
// and are therefore gated with `#[cfg(feature = "ssr")]` to prevent
|
||||||
// compilés dans le bundle WASM.
|
// 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;
|
pub mod routes;
|
||||||
|
|
||||||
// Modules SSR uniquement — utilisent des crates non disponibles en WASM
|
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
// These handlers complement the routes managed by Leptos.
|
||||||
// Exemples d'usages futurs :
|
// Intended for future use:
|
||||||
// - Endpoints API REST (/api/...)
|
// - REST API endpoints (/api/...)
|
||||||
// - Exports de fichiers (CSV, PDF...)
|
// - File exports (CSV, PDF...)
|
||||||
// - Webhooks entrants
|
// - Incoming webhooks
|
||||||
// - Health check pour le monitoring (/health)
|
// - Health check endpoint (/health)
|
||||||
//
|
//
|
||||||
// `#[cfg(feature = "ssr")]` protège tout ce fichier :
|
// The entire file is guarded by `#[cfg(feature = "ssr")]`:
|
||||||
// Axum n'existe pas dans le bundle WASM, donc on ne compile ce code qu'en mode serveur.
|
// Axum does not exist in the WASM bundle, so this code is server-only.
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
use axum::{http::StatusCode, response::IntoResponse};
|
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.
|
// `impl IntoResponse`: Axum accepts any type that implements this trait.
|
||||||
// Un tuple `(StatusCode, &str)` l'implémente automatiquement :
|
// A `(StatusCode, &str)` tuple implements it automatically:
|
||||||
// Axum en fait une réponse HTTP 404 avec le corps "Page introuvable".
|
// 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")]
|
#[cfg(feature = "ssr")]
|
||||||
pub async fn not_found_handler() -> impl IntoResponse {
|
pub async fn not_found_handler() -> impl IntoResponse {
|
||||||
(StatusCode::NOT_FOUND, "Page introuvable")
|
(StatusCode::NOT_FOUND, "Not found")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
// This module enforces rules that cannot be expressed through Rust's type system
|
||||||
// uniquement par les types Rust. Il s'exécute côté serveur uniquement
|
// alone. It runs server-side only because it uses `ipnetwork` for CIDR arithmetic.
|
||||||
// car il utilise `ipnetwork` pour les calculs CIDR.
|
|
||||||
|
|
||||||
use ipnetwork::IpNetwork;
|
use ipnetwork::IpNetwork;
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
// ─── Erreurs de validation ────────────────────────────────────────────────────
|
// ─── Validation errors ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum ValidationError {
|
pub enum ValidationError {
|
||||||
/// Le format CIDR est invalide (ex: "192.168.1/24" au lieu de "192.168.1.0/24")
|
/// The CIDR string is malformed (e.g. "192.168.1/24" instead of "192.168.1.0/24")
|
||||||
#[error("CIDR invalide '{0}' : {1}")]
|
#[error("Invalid CIDR '{0}': {1}")]
|
||||||
CidrInvalide(String, ipnetwork::IpNetworkError),
|
InvalidCidr(String, ipnetwork::IpNetworkError),
|
||||||
|
|
||||||
/// L'adresse IP n'a pas le bon format
|
/// The IP address string is malformed
|
||||||
#[error("Adresse IP invalide '{0}' : {1}")]
|
#[error("Invalid IP address '{0}': {1}")]
|
||||||
IpInvalide(String, std::net::AddrParseError),
|
InvalidIp(String, std::net::AddrParseError),
|
||||||
|
|
||||||
/// L'IP de l'hôte n'appartient pas à la plage du réseau
|
/// The host IP does not fall within the network's CIDR range
|
||||||
#[error("L'adresse IP {ip} n'appartient pas au réseau {cidr}")]
|
#[error("IP address {ip} does not belong to network {cidr}")]
|
||||||
IpHorsReseau { ip: String, cidr: String },
|
IpOutsideNetwork { ip: String, cidr: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Validation du CIDR ───────────────────────────────────────────────────────
|
// ─── CIDR validation ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Vérifie qu'une chaîne est un CIDR valide.
|
/// Validates that a string is a well-formed CIDR block.
|
||||||
/// Retourne le réseau parsé si valide.
|
/// Returns the parsed network on success.
|
||||||
///
|
///
|
||||||
/// `&str` : on emprunte la chaîne sans en prendre possession (pas de copie).
|
/// `&str`: borrowed string reference — no copy, just a borrow.
|
||||||
/// `Result<T, E>` : soit une valeur T (succès), soit une erreur E (échec).
|
/// `Result<T, E>`: either a value T (success) or an error E (failure).
|
||||||
pub fn valider_cidr(cidr: &str) -> Result<IpNetwork, ValidationError> {
|
pub fn validate_cidr(cidr: &str) -> Result<IpNetwork, ValidationError> {
|
||||||
cidr.parse::<IpNetwork>()
|
cidr.parse::<IpNetwork>()
|
||||||
// `.map_err` transforme l'erreur si `parse` échoue
|
// `.map_err` transforms the error if `parse` fails.
|
||||||
// `|e|` est une closure : une fonction anonyme qui prend `e` en paramètre
|
// `|e|` is a closure: an anonymous function that takes `e` as a parameter.
|
||||||
.map_err(|e| ValidationError::CidrInvalide(cidr.to_string(), e))
|
.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.
|
/// Key business rule: a host must always reside within its assigned network.
|
||||||
/// Ex : 192.168.1.10 ✓ dans 192.168.1.0/24
|
/// Example: 192.168.1.10 ✓ in 192.168.1.0/24
|
||||||
/// 10.0.0.1 ✗ dans 192.168.1.0/24
|
/// 10.0.0.1 ✗ in 192.168.1.0/24
|
||||||
pub fn valider_ip_dans_reseau(ip: &str, cidr: &str) -> Result<(), ValidationError> {
|
pub fn validate_ip_in_network(ip: &str, cidr: &str) -> Result<(), ValidationError> {
|
||||||
// On parse le CIDR et l'IP, propageant les erreurs avec `?`
|
let network = validate_cidr(cidr)?;
|
||||||
let reseau = valider_cidr(cidr)?;
|
|
||||||
|
|
||||||
let adresse: IpAddr = ip
|
let address: IpAddr = ip
|
||||||
.parse()
|
.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
|
// `IpNetwork::contains` returns true if the address falls within the range.
|
||||||
if reseau.contains(adresse) {
|
if network.contains(address) {
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(ValidationError::IpHorsReseau {
|
Err(ValidationError::IpOutsideNetwork {
|
||||||
ip: ip.to_string(),
|
ip: ip.to_string(),
|
||||||
cidr: cidr.to_string(),
|
cidr: cidr.to_string(),
|
||||||
})
|
})
|
||||||
@@ -67,44 +65,44 @@ pub fn valider_ip_dans_reseau(ip: &str, cidr: &str) -> Result<(), ValidationErro
|
|||||||
|
|
||||||
// ─── Tests ────────────────────────────────────────────────────────────────────
|
// ─── Tests ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// `#[cfg(test)]` : ce bloc n'est compilé que lors de `cargo test`
|
// `#[cfg(test)]`: this block is only compiled when running `cargo test`.
|
||||||
// Écrire les tests directement dans le même fichier est idiomatique en Rust.
|
// Writing tests in the same file as the code being tested is idiomatic Rust.
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
// `super::*` importe tout depuis le module parent (ce fichier)
|
// `super::*` imports everything from the parent module (this file).
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ip_dans_reseau_valide() {
|
fn ip_within_valid_network() {
|
||||||
// `.unwrap()` est acceptable dans les tests — si ça échoue, le test échoue
|
// `.unwrap()` is acceptable in tests — a failure here fails the test.
|
||||||
assert!(valider_ip_dans_reseau("192.168.1.10", "192.168.1.0/24").is_ok());
|
assert!(validate_ip_in_network("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!(validate_ip_in_network("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());
|
assert!(validate_ip_in_network("172.16.5.100", "172.16.0.0/12").is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ip_hors_reseau() {
|
fn ip_outside_network() {
|
||||||
assert!(valider_ip_dans_reseau("10.0.0.1", "192.168.1.0/24").is_err());
|
assert!(validate_ip_in_network("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());
|
assert!(validate_ip_in_network("192.168.2.1", "192.168.1.0/24").is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cidr_invalide() {
|
fn invalid_cidr() {
|
||||||
assert!(valider_cidr("pas-un-cidr").is_err());
|
assert!(validate_cidr("not-a-cidr").is_err());
|
||||||
assert!(valider_cidr("192.168.1/24").is_err()); // adresse tronquée
|
assert!(validate_cidr("192.168.1/24").is_err()); // truncated address
|
||||||
assert!(valider_cidr("192.168.1.0/33").is_err()); // préfixe > 32
|
assert!(validate_cidr("192.168.1.0/33").is_err()); // prefix > 32
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ip_invalide() {
|
fn invalid_ip() {
|
||||||
assert!(valider_ip_dans_reseau("999.0.0.1", "192.168.1.0/24").is_err());
|
assert!(validate_ip_in_network("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());
|
assert!(validate_ip_in_network("not-an-ip", "192.168.1.0/24").is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn protocole_connu() {
|
fn known_protocol() {
|
||||||
assert_eq!(crate::models::Port::protocole_connu(22), Some("SSH"));
|
assert_eq!(crate::models::Port::known_protocol(22), Some("SSH"));
|
||||||
assert_eq!(crate::models::Port::protocole_connu(443), Some("HTTPS"));
|
assert_eq!(crate::models::Port::known_protocol(443), Some("HTTPS"));
|
||||||
assert_eq!(crate::models::Port::protocole_connu(9999), None);
|
assert_eq!(crate::models::Port::known_protocol(9999), None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user