feat(models): add domain structs and CIDR validation

Add shared models (Network, Host, Port, Application, ApplicationPort)
with serde derives for Leptos server function serialization.
Add server/validation.rs with valider_ip_dans_reseau() and 5 unit tests.
Gate SSR-only modules (config, validation) with #[cfg(feature = "ssr")].

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-15 19:48:52 +02:00
parent b6d1e22d25
commit 4e9eab0450
6 changed files with 274 additions and 8 deletions

View File

@@ -1,13 +1,17 @@
// server/mod.rs — Module serveur
//
// Contient tout le code qui s'exécute uniquement côté serveur :
// - Handlers HTTP additionnels (routes.rs)
// - Fonctions serveur Leptos : accès à la base de données, authentification...
// - Logique métier qui ne doit JAMAIS être exposée dans le bundle WASM
// Contient tout le code qui s'exécute uniquement côté serveur.
//
// Les "server functions" Leptos (marquées #[server]) sont déclarées ici.
// Elles apparaissent comme des appels asynchrones normaux côté client,
// mais s'exécutent exclusivement sur le serveur — transparence totale.
// 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.
pub mod config;
// 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;
#[cfg(feature = "ssr")]
pub mod validation;

110
src/server/validation.rs Normal file
View File

@@ -0,0 +1,110 @@
// server/validation.rs — Validation des données métier
//
// 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.
use ipnetwork::IpNetwork;
use std::net::IpAddr;
use thiserror::Error;
// ─── Erreurs de validation ────────────────────────────────────────────────────
#[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),
/// L'adresse IP n'a pas le bon format
#[error("Adresse IP invalide '{0}' : {1}")]
IpInvalide(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 },
}
// ─── Validation du CIDR ───────────────────────────────────────────────────────
/// Vérifie qu'une chaîne est un CIDR valide.
/// Retourne le réseau parsé si valide.
///
/// `&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> {
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))
}
// ─── Validation de l'appartenance IP ↔ Réseau ────────────────────────────────
/// Vérifie qu'une adresse IP appartient à la plage d'un réseau CIDR.
///
/// 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)?;
let adresse: IpAddr = ip
.parse()
.map_err(|e| ValidationError::IpInvalide(ip.to_string(), e))?;
// `IpNetwork::contains` retourne true si l'IP est dans la plage
if reseau.contains(adresse) {
Ok(())
} else {
Err(ValidationError::IpHorsReseau {
ip: ip.to_string(),
cidr: cidr.to_string(),
})
}
}
// ─── 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)]
mod tests {
// `super::*` importe tout depuis le module parent (ce fichier)
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());
}
#[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());
}
#[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
}
#[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());
}
#[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);
}
}