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:
@@ -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()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user