backend/src/domain/entities/user.rs
Description
Entité domaine représentant un utilisateur de la plateforme Koprogo. Cette entité constitue le cœur du système d’authentification et de gestion des permissions multi-rôles et multi-tenant.
Responsabilités
Modélisation utilisateur - Identité et informations personnelles - Gestion des rôles et permissions - Association avec une organisation
Validation métier - Validation de l’email (format valide) - Validation des noms (longueur minimale) - Normalisation des données (trim, lowercase pour email)
Logique métier - Activation/désactivation du compte - Mise à jour du profil - Contrôle d’accès multi-tenant
Énumérations
UserRole
Signature:
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum UserRole {
SuperAdmin,
Syndic,
Accountant,
Owner,
}
Description:
Énumération des rôles disponibles dans le système.
Variantes:
Rôle |
Description |
|---|---|
|
Administrateur plateforme avec accès illimité à toutes les organisations |
|
Gestionnaire de copropriété avec accès complet aux immeubles de son organisation |
|
Comptable avec accès aux données financières de son organisation |
|
Copropriétaire avec accès limité à ses propres lots et informations |
Traits implémentés:
Display- Conversion en chaîne lowercase (ex: “superadmin”, “syndic”)FromStr- Parsing depuis chaîne avec gestion d’erreursSerialize/Deserialize- Sérialisation JSON via SerdePartialEq/Eq- Comparaison
Exemples:
use std::str::FromStr;
// Conversion en String
let role = UserRole::Syndic;
assert_eq!(role.to_string(), "syndic");
// Parsing depuis String
let role = UserRole::from_str("accountant").unwrap();
assert_eq!(role, UserRole::Accountant);
// Erreur pour rôle invalide
let result = UserRole::from_str("invalid");
assert!(result.is_err());
Structures
User
Signature:
#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
pub struct User {
pub id: Uuid,
#[validate(email(message = "Email must be valid"))]
pub email: String,
#[serde(skip_serializing)]
pub password_hash: String,
#[validate(length(min = 2, message = "First name must be at least 2 characters"))]
pub first_name: String,
#[validate(length(min = 2, message = "Last name must be at least 2 characters"))]
pub last_name: String,
pub role: UserRole,
pub organization_id: Option<Uuid>,
pub is_active: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
Description:
Représente un utilisateur avec ses informations personnelles, son rôle et son affiliation organisationnelle.
Champs:
Champ |
Type |
Description |
|---|---|---|
|
|
Identifiant unique UUID v4 |
|
|
Email (unique, validé, normalisé en lowercase) |
|
|
Hash bcrypt du mot de passe (non sérialisé dans JSON) |
|
|
Prénom (minimum 2 caractères) |
|
|
Nom de famille (minimum 2 caractères) |
|
|
Rôle déterminant les permissions |
|
|
ID organisation (None pour SuperAdmin) |
|
|
Indicateur d’activation du compte |
|
|
Date de création (UTC) |
|
|
Date de dernière modification (UTC) |
Validations automatiques:
Email: Format RFC 5322
Prénom/Nom: Longueur >= 2 caractères
Email normalisé: trim() + to_lowercase()
Noms normalisés: trim()
Méthodes
new()
Signature:
pub fn new(
email: String,
password_hash: String,
first_name: String,
last_name: String,
role: UserRole,
organization_id: Option<Uuid>,
) -> Result<Self, String>
Description:
Constructeur qui crée un nouvel utilisateur avec validation automatique.
Comportement:
Génère un UUID v4 unique
Normalise l’email (lowercase + trim)
Normalise les noms (trim)
Active le compte par défaut (
is_active = true)Initialise les timestamps à
Utc::now()Exécute les validations (email format, longueur noms)
Paramètres:
email- Adresse email (sera normalisée)password_hash- Hash bcrypt du mot de passefirst_name- Prénom de l’utilisateurlast_name- Nom de famillerole- Rôle dans le systèmeorganization_id- ID organisation (Nonepour SuperAdmin)
Retour:
Ok(User)- Utilisateur créé avec succèsErr(String)- Message d’erreur de validation
Exemples:
use bcrypt::{hash, DEFAULT_COST};
// ✅ Création réussie
let password_hash = hash("password123", DEFAULT_COST).unwrap();
let user = User::new(
" JOHN.DOE@EXAMPLE.COM ".to_string(), // Sera normalisé
password_hash,
" John ".to_string(), // Sera trim
"Doe".to_string(),
UserRole::Syndic,
Some(Uuid::new_v4()),
);
assert!(user.is_ok());
assert_eq!(user.unwrap().email, "john.doe@example.com");
// ❌ Email invalide
let result = User::new(
"invalid-email".to_string(),
password_hash,
"John".to_string(),
"Doe".to_string(),
UserRole::Syndic,
None,
);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Email must be valid"));
// ❌ Prénom trop court
let result = User::new(
"valid@example.com".to_string(),
password_hash,
"J".to_string(),
"Doe".to_string(),
UserRole::Syndic,
None,
);
assert!(result.is_err());
full_name()
Signature:
pub fn full_name(&self) -> String
Description:
Retourne le nom complet de l’utilisateur (prénom + nom).
Retour:
Chaîne formatée: "{first_name} {last_name}"
Exemple:
let user = User::new(
"john@example.com".to_string(),
"hash".to_string(),
"John".to_string(),
"Doe".to_string(),
UserRole::Syndic,
None,
).unwrap();
assert_eq!(user.full_name(), "John Doe");
update_profile()
Signature:
pub fn update_profile(&mut self, first_name: String, last_name: String) -> Result<(), String>
Description:
Met à jour le prénom et le nom de l’utilisateur avec validation.
Comportement:
Normalise les nouveaux noms (trim)
Met à jour
first_nameetlast_nameMet à jour
updated_atàUtc::now()Valide les nouvelles valeurs
Paramètres:
first_name- Nouveau prénomlast_name- Nouveau nom
Retour:
Ok(())- Mise à jour réussieErr(String)- Erreur de validation
Exemple:
let mut user = User::new(/* ... */).unwrap();
let result = user.update_profile("Jane".to_string(), "Smith".to_string());
assert!(result.is_ok());
assert_eq!(user.full_name(), "Jane Smith");
// Le timestamp est mis à jour
// assert!(user.updated_at > old_timestamp);
deactivate()
Signature:
pub fn deactivate(&mut self)
Description:
Désactive le compte utilisateur. Un compte désactivé ne peut plus se connecter.
Comportement:
Définit
is_activeàfalseMet à jour
updated_at
Exemple:
let mut user = User::new(/* ... */).unwrap();
assert!(user.is_active);
user.deactivate();
assert!(!user.is_active);
activate()
Signature:
pub fn activate(&mut self)
Description:
Réactive un compte utilisateur précédemment désactivé.
Comportement:
Définit
is_activeàtrueMet à jour
updated_at
Exemple:
let mut user = User::new(/* ... */).unwrap();
user.deactivate();
user.activate();
assert!(user.is_active);
can_access_building()
Signature:
pub fn can_access_building(&self, building_org_id: Option<Uuid>) -> bool
Description:
Vérifie si l’utilisateur peut accéder à un immeuble donné selon son rôle et son organisation.
Logique d’accès:
┌─────────────────────────────────────────────────────────┐
│ Utilisateur SuperAdmin ? │
│ ├─ Oui → ✅ Accès autorisé (accès universel) │
│ └─ Non → Vérifier organization_id │
│ └─ self.organization_id == building_org_id ? │
│ ├─ Oui → ✅ Accès autorisé (même org) │
│ └─ Non → ❌ Accès refusé (org différente) │
└─────────────────────────────────────────────────────────┘
Paramètres:
building_org_id- ID de l’organisation propriétaire de l’immeuble
Retour:
true- L’utilisateur peut accéder à l’immeublefalse- Accès refusé
Exemples:
// SuperAdmin: accès universel
let superadmin = User::new(
"admin@example.com".to_string(),
"hash".to_string(),
"Admin".to_string(),
"User".to_string(),
UserRole::SuperAdmin,
None,
).unwrap();
assert!(superadmin.can_access_building(Some(Uuid::new_v4())));
assert!(superadmin.can_access_building(None));
// Utilisateur régulier: accès limité à son organisation
let org_id = Uuid::new_v4();
let syndic = User::new(
"syndic@example.com".to_string(),
"hash".to_string(),
"John".to_string(),
"Syndic".to_string(),
UserRole::Syndic,
Some(org_id),
).unwrap();
assert!(syndic.can_access_building(Some(org_id))); // ✅ Même org
assert!(!syndic.can_access_building(Some(Uuid::new_v4()))); // ❌ Autre org
assert!(!syndic.can_access_building(None)); // ❌ Pas d'org
Tests unitaires
Le fichier contient 7 tests unitaires couvrant:
Test |
Scénario couvert |
|---|---|
|
Création réussie avec données valides |
|
Rejet email invalide |
|
Mise à jour prénom/nom |
|
Désactivation compte |
|
SuperAdmin: accès universel |
|
Utilisateur régulier: accès limité |
Exécuter les tests:
cd backend
cargo test domain::entities::user
Architecture Multi-tenant
La structure User implémente le pattern Multi-tenancy via organization_id:
┌───────────────────────────────────────────────────────┐
│ Organization A │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ Syndic │ │ Accountant │ │ Owner 1 │ │
│ │ User │ │ User │ │ User │ │
│ └────────────┘ └────────────┘ └────────────┘ │
└───────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────┐
│ Organization B │
│ ┌────────────┐ ┌────────────┐ │
│ │ Syndic │ │ Owner 2 │ │
│ │ User │ │ User │ │
│ └────────────┘ └────────────┘ │
└───────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────┐
│ SuperAdmin │
│ (organization_id = None) │
│ ├─ Accès Organization A │
│ ├─ Accès Organization B │
│ └─ Accès toutes les organisations │
└───────────────────────────────────────────────────────┘
Hiérarchie des permissions
SuperAdmin (plateforme)
↓
┌───────────────────────────────────────┐
│ Organisation │
│ │
│ Syndic (gestion complète) │
│ ↓ │
│ Accountant (finance uniquement) │
│ ↓ │
│ Owner (consultation limitée) │
└───────────────────────────────────────┘
Dépendances
Crates externes:
uuid- Génération d’identifiants uniqueschrono- Gestion des timestamps UTCserde- Sérialisation JSONvalidator- Validation déclarative (email, longueur)
Modules internes:
Aucun (entité auto-suffisante)
Utilisation dans l’application
Création d’un utilisateur (use case):
use bcrypt::{hash, DEFAULT_COST};
// Hash du mot de passe
let password_hash = hash("user_password", DEFAULT_COST)?;
// Création de l'entité
let user = User::new(
"user@example.com".to_string(),
password_hash,
"John".to_string(),
"Doe".to_string(),
UserRole::Syndic,
Some(organization_id),
)?;
// Sauvegarde via repository
user_repository.create(user).await?;
Authentification (JWT):
// Vérification du mot de passe
let user = user_repository.find_by_email(email).await?;
let valid = bcrypt::verify(password, &user.password_hash)?;
if valid && user.is_active {
// Générer token JWT
let claims = Claims {
sub: user.id.to_string(),
role: user.role.to_string(),
org: user.organization_id.map(|id| id.to_string()),
exp: /* ... */,
};
let token = encode(&Header::default(), &claims, &encoding_key)?;
}
Contrôle d’accès:
// Dans un handler
let building = building_repository.find_by_id(building_id).await?;
if !current_user.can_access_building(building.organization_id) {
return Err(Error::Forbidden);
}
Notes de sécurité
Warning
Password Hash:
Le champ password_hash utilise #[serde(skip_serializing)] pour éviter de l’exposer dans les réponses JSON. Assurez-vous de:
Utiliser bcrypt avec cost >= 12
Ne JAMAIS logger le password_hash
Ne JAMAIS l’inclure dans les réponses API
Warning
Désactivation vs Suppression:
Utilisez deactivate() plutôt que de supprimer les utilisateurs pour:
Préserver l’intégrité référentielle
Garder l’historique des actions
Possibilité de réactivation
Fichiers associés
backend/src/domain/entities/organization.rs- Entité Organisationbackend/src/application/ports/user_repository.rs- Trait repositorybackend/src/infrastructure/database/repositories/user_repository_impl.rs- Implémentation PostgreSQLbackend/src/application/use_cases/auth_use_cases.rs- Cas d’usage authentificationbackend/src/application/dto/auth_dto.rs- DTOs pour authentification