DEVELOPER.md 76890 octets

Guide du developpeur — Rustwarden

Ce document est le guide de reference pour comprendre, etendre et contribuer au framework Rustwarden.


Table des matieres

  1. Architecture globale
  2. rustwarden-core en detail
  3. rustwarden-ui en detail
  4. Ajouter des routes
  5. Ajouter un modele SeaORM
  6. Ajouter une migration
  7. Ajouter un service
  8. Personnaliser les templates
  9. Authentification et autorisation
  10. RBAC : roles et permissions
  11. Authentification OAuth / SSO
  12. Hooks : intercepter les evenements
  13. Internationalisation (i18n)
  14. Taches de fond
  15. Rate limiting et notifications temps-reel
  16. Middleware
  17. Configuration
  18. Commandes binaires
  19. Le CLI wizard
  20. Tests
  21. Production

Architecture globale

Le projet est organise en Cargo workspace avec 4 membres : le coeur du framework (rustwarden-core), l'interface web (rustwarden-ui), l'application de reference (app) et l'outil CLI (rustwarden-cli).

Sources de verite :

  • Templates : crates/rustwarden-ui/templates/ (compile par Askama)
  • Migrations : crates/rustwarden-core/migrations/ (compile par SeaORM)
  • Traductions : locales/ a la racine (charge a l'execution par I18nService)

Les anciens miroirs ont ete renommes templates_old/, migrations_old/, crates/rustwarden-ui/locales_old/ et peuvent etre supprimes.


Arborescence complete et role de chaque fichier

rustwarden/
│
├── Cargo.toml                    # Manifeste du workspace (members, resolver, lints)
├── Cargo.lock                    # Verrouillage des versions de dependances
├── .env                          # Variables d'environnement locales (non versionne)
├── .env.sample                   # Exemple de .env distribue avec le projet
├── .gitignore                    # Exclusions Git (target/, .env, data/, etc.)
├── README.md                     # Presentation du projet, quick-start, variables d'env
├── DEVELOPER.md                  # Ce guide developpeur
├── constitution.md               # Constitution du projet (regles, vision)
├── tailwind.config.js            # Configuration Tailwind CSS + DaisyUI
├── package.json                  # Dependances Node (tailwind, daisyui)
├── package-lock.json             # Lock Node
├── certificat_imap.pem           # Certificat TLS pour la connexion IMAP
├── Todo.old                      # Archive de l'ancien fichier todo
│
├── app/                          ──── APPLICATION DE REFERENCE ────
│   ├── Cargo.toml                # Dependances : rustwarden-core, rustwarden-ui, tokio, anyhow
│   ├── src/
│   │   └── main.rs               # Point d'entree minimal : RustwardenApp::builder().from_env()
│   └── examples/
│       ├── minimal_headless.rs   # Exemple sans UI (API-only, headless)
│       ├── with_default_ui.rs    # Exemple avec l'UI par defaut
│       └── custom_hooks.rs       # Exemple d'implementation de RustwardenHooks
│
├── crates/
│   │
│   ├── rustwarden-core/          ──── COEUR DU FRAMEWORK ────
│   │   ├── Cargo.toml            # Dependances (axum, sea-orm, jsonwebtoken, bcrypt, aes-gcm...)
│   │   │                           feature "oauth" active openidconnect + reqwest
│   │   │
│   │   ├── src/
│   │   │   ├── lib.rs            # Point d'entree de la lib : re-exports de tous les modules
│   │   │   │                       + module `prelude` (RustwardenApp, AuthUser, AppError, etc.)
│   │   │   ├── app.rs            # Builder pattern RustwardenApp : configure DB, routes, hooks,
│   │   │   │                       middleware, i18n, taches de fond, puis lance le serveur
│   │   │   ├── hooks.rs          # Trait RustwardenHooks (on_user_registered, on_user_login,
│   │   │   │                       on_password_changed, on_oauth_login, on_resource_shared...)
│   │   │   │                       + struct NoopHooks (implementation par defaut)
│   │   │   ├── error.rs          # Enum AppError : types d'erreur unifies (400, 401, 403, 404, 500)
│   │   │   ├── migrations.rs     # Re-export du Migrator SeaORM pour les applications externes
│   │   │   │
│   │   │   ├── auth/             ── Authentification ──
│   │   │   │   ├── mod.rs        # Re-exports du module auth
│   │   │   │   ├── jwt.rs        # JwtService : generation/validation des tokens JWT (access + refresh)
│   │   │   │   ├── jwt_extractor.rs  # Extracteur Axum pour valider le JWT depuis les cookies/headers
│   │   │   │   ├── session.rs    # SessionConfig : configuration des sessions (duree, cookie, etc.)
│   │   │   │   ├── extractors.rs # AuthUser : extracteur qui injecte l'utilisateur authentifie
│   │   │   │   ├── handlers.rs   # Handlers API REST : login, register, logout, refresh_token,
│   │   │   │   │                   forgot_password, reset_password, verify_email, resend_verification
│   │   │   │   └── oauth_handlers.rs  # [feature: oauth] Handlers OAuth : initier le flow,
│   │   │   │                            callback provider, lier/delier un compte
│   │   │   │
│   │   │   ├── config/           ── Configuration ──
│   │   │   │   ├── mod.rs        # Re-exports des sous-modules config
│   │   │   │   ├── app.rs        # APP_NAME, APP_DOMAIN, APP_DESCRIPTION (depuis env ou defaut)
│   │   │   │   ├── server.rs     # ServerConfig : HOST, PORT, RUST_LOG
│   │   │   │   ├── database.rs   # Connexion a PostgreSQL via DATABASE_URL
│   │   │   │   ├── auth.rs       # AuthConfig : JWT_SECRET, durees de token, bcrypt cost
│   │   │   │   ├── email.rs      # EmailConfig : SMTP + IMAP, parametres d'envoi
│   │   │   │   ├── admin.rs      # DEFAULT_ADMIN_* : creation auto du premier admin
│   │   │   │   ├── tasks.rs      # TasksConfig : intervalles des taches de fond
│   │   │   │   └── oauth.rs      # [feature: oauth] OAuthConfig : providers, client_id/secret,
│   │   │   │                       chargement depuis DB (dechiffrement AES-256-GCM) + fallback env
│   │   │   │
│   │   │   ├── middleware/       ── Middleware Axum ──
│   │   │   │   ├── mod.rs        # Re-exports
│   │   │   │   ├── auth_required.rs   # Layer qui exige un JWT valide
│   │   │   │   ├── auth_service.rs    # Service qui attache AuthUser a la requete
│   │   │   │   ├── admin_required.rs  # Layer qui exige le role "admin"
│   │   │   │   ├── admin_service.rs   # Service pour la verification admin
│   │   │   │   ├── admin_extractor.rs # Extracteur AdminUser
│   │   │   │   ├── admin_helper.rs    # Fonctions utilitaires admin
│   │   │   │   ├── csrf.rs            # Protection CSRF (generation + validation de tokens)
│   │   │   │   ├── rate_limit.rs      # Rate limiting par IP (configurable)
│   │   │   │   ├── password_complexity.rs  # Validation de complexite de mot de passe
│   │   │   │   ├── update_last_login.rs    # Met a jour last_login_at apres chaque login
│   │   │   │   └── error_handler.rs   # Middleware de gestion globale des erreurs
│   │   │   │
│   │   │   ├── models/           ── Entites SeaORM (ORM PostgreSQL) ──
│   │   │   │   ├── mod.rs               # Re-exports de toutes les entites
│   │   │   │   ├── user.rs              # Utilisateur : id, username, email, password_hash,
│   │   │   │   │                          must_change_password, email_verified, timestamps
│   │   │   │   ├── role.rs              # Role : id, name, description
│   │   │   │   ├── user_role.rs         # Relation N:N users <-> roles
│   │   │   │   ├── permission.rs        # Permission : id, name, description
│   │   │   │   ├── role_permission.rs   # Relation N:N roles <-> permissions
│   │   │   │   ├── app_settings.rs      # Parametres applicatifs cle/valeur persistants
│   │   │   │   ├── audit_log.rs         # Journal d'audit (action, user_id, ip, details)
│   │   │   │   ├── refresh_token.rs     # Tokens de rafraichissement JWT (hashes SHA-256)
│   │   │   │   ├── jwt_blacklist.rs     # Blacklist des JWT revoques
│   │   │   │   ├── email_queue.rs       # File d'attente d'envoi d'emails
│   │   │   │   ├── email_delivery_status.rs  # Statut de livraison des emails
│   │   │   │   ├── email_validation_token.rs # Tokens de validation d'email
│   │   │   │   ├── password_change_request.rs # Demandes de changement de mot de passe (double confirm.)
│   │   │   │   ├── password_reset_token.rs    # Tokens de reinitialisation de mot de passe
│   │   │   │   ├── user_preferences.rs  # Preferences utilisateur (theme, langue, timezone)
│   │   │   │   ├── resource.rs          # Ressource generique (type, owner_id, metadata JSON)
│   │   │   │   ├── resource_share.rs    # Partage de ressource (resource_id, user_id, permission_level)
│   │   │   │   └── oauth_account.rs     # [feature: oauth] Compte OAuth lie (provider, provider_user_id)
│   │   │   │
│   │   │   ├── services/         ── Logique metier ──
│   │   │   │   ├── mod.rs                    # Re-exports de tous les services
│   │   │   │   ├── user_service.rs           # CRUD utilisateurs, recherche, listing pagine
│   │   │   │   ├── auth_service.rs           # Logique d'authentification (login, register, verify)
│   │   │   │   ├── auth_logging.rs           # Journalisation des evenements d'authentification
│   │   │   │   ├── role_service.rs           # CRUD roles, assignation aux utilisateurs
│   │   │   │   ├── permission_service.rs     # CRUD permissions, verification d'acces
│   │   │   │   ├── password_service.rs       # Hachage bcrypt, verification de mot de passe
│   │   │   │   ├── password_change_service.rs  # Workflow de changement de mdp (double confirmation email)
│   │   │   │   ├── password_reset_service.rs   # Workflow "mot de passe oublie" (token par email)
│   │   │   │   ├── email_service.rs          # Envoi d'emails via SMTP (lettre)
│   │   │   │   ├── email_queue_service.rs    # Gestion de la file d'attente d'emails
│   │   │   │   ├── email_validation_service.rs # Validation d'adresse email (token + verification)
│   │   │   │   ├── email_sanitization.rs     # Normalisation et nettoyage des adresses email
│   │   │   │   ├── imap_bounce_service.rs    # Detection des bounces via IMAP
│   │   │   │   ├── jwt_blacklist_service.rs  # Gestion de la blacklist JWT
│   │   │   │   ├── refresh_token_service.rs  # CRUD refresh tokens (stockage hash SHA-256)
│   │   │   │   ├── app_settings_service.rs   # Lecture/ecriture des parametres applicatifs (DB)
│   │   │   │   ├── default_admin_service.rs  # Creation automatique du premier admin au demarrage
│   │   │   │   ├── crypto_service.rs         # Chiffrement AES-256-GCM (secrets OAuth en DB)
│   │   │   │   │                               utilise OAUTH_ENCRYPTION_KEY (hex 64 chars)
│   │   │   │   ├── user_preferences_service.rs # CRUD preferences utilisateur
│   │   │   │   ├── resource_service.rs       # CRUD ressources generiques + partage
│   │   │   │   └── oauth/                    # [feature: oauth] ── Services OAuth ──
│   │   │   │       ├── mod.rs                # Re-exports OAuthProvider, OAuthService
│   │   │   │       ├── provider_trait.rs     # Trait OAuthProvider + struct ProviderUserInfo
│   │   │   │       ├── service.rs            # OAuthService : orchestration du flow OAuth/OIDC
│   │   │   │       │                           (generation URL, echange de code, creation/liaison compte)
│   │   │   │       └── providers/
│   │   │   │           ├── mod.rs            # ProviderRegistry : resolution dynamique des providers
│   │   │   │           ├── google.rs         # GoogleProvider (OpenID Connect)
│   │   │   │           ├── github.rs         # GitHubProvider (OAuth2 + API /user)
│   │   │   │           ├── discord.rs        # DiscordProvider (OAuth2 + API /users/@me)
│   │   │   │           └── microsoft.rs      # MicrosoftProvider (OpenID Connect, multi-tenant)
│   │   │   │
│   │   │   ├── tasks/            ── Taches de fond (tokio::spawn) ──
│   │   │   │   ├── mod.rs                        # Re-exports
│   │   │   │   ├── email_queue_processor.rs       # Traitement periodique de la file d'emails
│   │   │   │   ├── password_change_cleanup.rs     # Nettoyage des demandes de changement expirees
│   │   │   │   └── password_change_email_retry.rs # Retry d'envoi d'emails de confirmation
│   │   │   │
│   │   │   ├── templates/        ── Utilitaires templates ──
│   │   │   │   ├── mod.rs              # Macro impl_template_response!, struct LayoutContext
│   │   │   │   └── email_templates.rs  # Structures Askama pour les templates d'emails
│   │   │   │
│   │   │   ├── i18n/             ── Internationalisation ──
│   │   │   │   ├── mod.rs        # Re-exports (Locale, I18nService)
│   │   │   │   ├── service.rs    # I18nService : traduction par cle, interpolation, pluralisation
│   │   │   │   ├── loader.rs     # Chargement des fichiers YAML de traduction
│   │   │   │   ├── extractor.rs  # Extracteur Axum Locale (cookie, header Accept-Language, defaut)
│   │   │   │   ├── helpers.rs    # Fonctions utilitaires i18n
│   │   │   │   └── middleware.rs # Middleware d'injection de la locale dans la requete
│   │   │   │
│   │   │   └── bin/              ── Binaires utilitaires ──
│   │   │       ├── seed.rs            # Peuplement initial de la base (roles, permissions, admin)
│   │   │       ├── hash_password.rs   # Outil CLI pour hasher un mot de passe bcrypt
│   │   │       └── change_password.rs # Outil CLI pour changer un mot de passe en DB
│   │   │
│   │   ├── templates/            ── Templates email HTML (compiles par Askama) ──
│   │   │   └── emails/
│   │   │       ├── welcome.html                    # Email de bienvenue
│   │   │       ├── email_validation.html           # Email de validation d'adresse
│   │   │       ├── password_reset.html             # Email de reinitialisation de mot de passe
│   │   │       ├── password_changed.html           # Notification de changement effectue
│   │   │       ├── password_change_confirmation.html # Demande de confirmation de changement
│   │   │       ├── account_modified.html           # Notification de modification de compte
│   │   │       └── admin_new_user.html             # Notification admin : nouvel utilisateur
│   │   │
│   │   └── migrations/           ── Migrations SeaORM ──
│   │       └── src/
│   │           ├── lib.rs         # Migrator : liste ordonnee des migrations
│   │           ├── main.rs        # Binaire pour executer les migrations manuellement
│   │           ├── m20260305_000001_initial_schema.rs       # Schema initial : users, roles, user_roles,
│   │           │                                              refresh_tokens, jwt_blacklist, email_queue,
│   │           │                                              email_delivery_status, email_validation_tokens,
│   │           │                                              password_reset_tokens, password_change_requests,
│   │           │                                              audit_log, user_preferences
│   │           ├── m20260306_000002_create_app_settings_table.rs  # Table app_settings (cle/valeur)
│   │           ├── m20260309_000003_create_permissions_tables.rs  # Tables permissions, role_permissions
│   │           ├── m20260309_000004_create_resources_tables.rs    # Tables resources, resource_shares
│   │           └── m20260310_000005_create_oauth_accounts_table.rs # Table oauth_accounts
│   │
│   └── rustwarden-ui/            ──── INTERFACE WEB (UI) ────
│       ├── Cargo.toml            # Dependances : rustwarden-core, askama, axum, tower-http
│       │
│       ├── src/
│       │   ├── lib.rs            # Exports : default_routes(), default_static_service()
│       │   ├── routes.rs         # Definition de toutes les routes HTTP (API + pages)
│       │   │                       GET/POST/PUT/DELETE pour auth, users, admin, settings, public
│       │   ├── extractors.rs     # Extracteurs specifiques a l'UI
│       │   ├── template_helpers.rs  # Fonctions utilitaires pour les templates Askama
│       │   │
│       │   ├── handlers/         ── Handlers Axum (un par domaine fonctionnel) ──
│       │   │   ├── mod.rs              # Re-exports de tous les handlers
│       │   │   ├── root_handler.rs     # Redirection / → /home ou /login
│       │   │   ├── home_handler.rs     # Page d'accueil authentifiee /home
│       │   │   ├── login_handler.rs    # Pages /login, /resend-verification, /reset-password
│       │   │   ├── settings_handler.rs # Pages /profile, /settings/password, /settings/sessions,
│       │   │   │                         /settings/preferences + handlers PUT associes
│       │   │   ├── admin_handler.rs    # Dashboard admin, CRUD users, CRUD roles, settings
│       │   │   │                         app_settings (GET/PUT) incluant la section OAuth
│       │   │   ├── dashboard_handler.rs # Donnees du dashboard admin (stats, graphiques)
│       │   │   ├── user_handler.rs     # API CRUD utilisateurs (/api/v1/users)
│       │   │   ├── email_handler.rs    # Logique liee aux emails dans l'UI
│       │   │   ├── navigation_handler.rs # Construction dynamique de la navigation
│       │   │   └── health.rs           # Endpoint /health (healthcheck)
│       │   │
│       │   └── dto/              ── Data Transfer Objects ──
│       │       ├── mod.rs              # Re-exports de tous les DTO
│       │       ├── admin_dto.rs        # DashboardStats, UserInfo, AppSettingsResponse, etc.
│       │       ├── auth_dto.rs         # LoginRequest, RegisterRequest, TokenResponse, etc.
│       │       ├── home_dto.rs         # HomePageData
│       │       ├── navigation_dto.rs   # NavItem, NavigationData
│       │       ├── sidebar_dto.rs      # SidebarItem, SidebarSection
│       │       ├── breadcrumb_dto.rs   # BreadcrumbItem
│       │       ├── settings_dto.rs     # UpdatePreferencesRequest, UserPreferencesResponse
│       │       ├── email_dto.rs        # EmailTemplateData
│       │       ├── user_dto.rs         # UserCreateRequest, UserUpdateRequest, etc.
│       │       └── user_profile_dto.rs # UserProfileDTO
│       │
│       ├── templates/            ── Templates HTML Askama (ceux-ci font foi) ──
│       │   ├── base.html               # Layout de base : <html>, <head>, <body>, DaisyUI/Tailwind
│       │   ├── base_with_theme.html    # Variante de base.html avec selection de theme
│       │   ├── home.html               # Page d'accueil (/home) apres connexion
│       │   ├── login.html              # Connexion + inscription + boutons OAuth
│       │   ├── profile.html            # Page de profil utilisateur
│       │   ├── resend_verification.html # Re-envoi d'email de verification
│       │   ├── reset_password.html     # Formulaire de reinitialisation de mot de passe
│       │   ├── email_verified.html     # Page de confirmation d'email verifie
│       │   │
│       │   ├── components/             # Composants reutilisables
│       │   │   ├── navbar.html         # Barre de navigation principale
│       │   │   ├── header.html         # En-tete de page
│       │   │   ├── navigation.html     # Menu de navigation lateral
│       │   │   ├── sidebar.html        # Barre laterale (admin, settings)
│       │   │   ├── breadcrumb.html     # Fil d'Ariane
│       │   │   ├── user_menu.html      # Menu utilisateur (avatar, deconnexion)
│       │   │   └── password_change_badge.html  # Badge "changement de mdp en attente"
│       │   │
│       │   ├── settings/               # Pages de parametres utilisateur
│       │   │   ├── password.html       # Changement de mot de passe
│       │   │   ├── preferences.html    # Preferences (theme, langue, timezone)
│       │   │   └── sessions.html       # Gestion des sessions actives
│       │   │
│       │   ├── admin/                  # Pages d'administration
│       │   │   ├── dashboard.html      # Tableau de bord admin (stats)
│       │   │   ├── users_list.html     # Liste des utilisateurs (pagination, recherche)
│       │   │   ├── user_form.html      # Formulaire creation/edition d'utilisateur
│       │   │   ├── roles_list.html     # Liste des roles
│       │   │   ├── role_form.html      # Formulaire creation/edition de role + permissions
│       │   │   ├── settings.html       # Parametres applicatifs (general, securite, audit, OAuth)
│       │   │   └── errors.html         # Page de visualisation des erreurs admin
│       │   │
│       │   ├── emails/                 # Templates d'emails (miroir de rustwarden-core/templates/emails)
│       │   │   ├── welcome.html
│       │   │   ├── email_validation.html
│       │   │   ├── password_reset.html
│       │   │   ├── password_changed.html
│       │   │   ├── password_change_confirmation.html
│       │   │   ├── account_modified.html
│       │   │   └── admin_new_user.html
│       │   │
│       │   ├── errors/                 # Pages d'erreur HTTP
│       │   │   ├── 401.html            # Non authentifie
│       │   │   ├── 403.html            # Acces interdit
│       │   │   └── 500.html            # Erreur serveur interne
│       │   │
│       │   ├── fragments/              # Fragments HTML (HTMX / inclusion partielle)
│       │   │   ├── form_message.html         # Message flash (succes/erreur) dans un formulaire
│       │   │   └── user_menu_dropdown.html   # Dropdown du menu utilisateur
│       │   │
│       │   └── sharing/                # Templates de partage de ressources (a venir)
│       │
│       └── locales/              ── Traductions YAML (crate UI) ──
│           ├── fr.yml                  # Francais
│           ├── en.yml                  # Anglais
│           ├── de.yml                  # Allemand
│           └── es.yml                  # Espagnol
│
├── locales/                      ──── TRADUCTIONS YAML (SOURCE DE VERITE) ────
│   ├── fr.yml                    # Francais
│   ├── en.yml                    # Anglais
│   ├── de.yml                    # Allemand
│   └── es.yml                    # Espagnol
│     NOTE: Ce dossier est charge a l'execution par le I18nService.
│     C'est la source de verite pour les traductions.
│
├── static/                       ──── FICHIERS STATIQUES ────
│   ├── css/
│   │   ├── tailwind.src.css      # Source Tailwind (directives @tailwind)
│   │   ├── tailwind.css          # CSS compile (genere par `npx tailwindcss`)
│   │   └── custom.css            # Styles personnalises supplementaires
│   ├── js/
│   │   ├── htmx.min.js           # HTMX (interactions dynamiques sans JS custom)
│   │   └── alpine.min.js         # Alpine.js (reactivite legere cote client)
│   └── images/
│       ├── logo.svg              # Logo de l'application
│       └── favicon.svg           # Favicon
│
├── templates_old/                ──── ARCHIVE (ancien miroir templates, ne pas utiliser) ────
├── migrations_old/               ──── ARCHIVE (ancien miroir migrations, ne pas utiliser) ────
│
├── database/                     ──── BASE DE DONNEES (dev) ────
│   ├── docker-compose.yml        # Docker Compose pour PostgreSQL local
│   ├── .env                      # Variables d'env pour le conteneur Docker
│   ├── env.sample                # Exemple de .env Docker
│   └── data/
│       └── postgres_data/        # Donnees PostgreSQL persistantes (volume Docker)
│
├── template/                     ──── TEMPLATE cargo-generate ────
│   ├── cargo-generate.toml       # Configuration cargo-generate (variables, placeholders)
│   ├── Cargo.toml                # Manifeste template avec {{project-name}}
│   ├── .env.sample               # Exemple de .env pour les projets generes
│   └── src/
│       └── main.rs               # main.rs template avec builder pattern
│
├── tools/                        ──── OUTILS ────
│   └── rustwarden-cli/
│       ├── Cargo.toml            # Dependances CLI : clap, dialoguer, console, rand
│       ├── README.md             # Documentation du CLI wizard
│       └── src/
│           ├── main.rs           # Point d'entree CLI (commande `start-project`)
│           ├── start_project.rs  # Wizard interactif : nom, features, DB, OAuth, generation
│           └── templates.rs      # Generateur de fichiers (Cargo.toml, main.rs, .env, etc.)
│
├── docs/                         ──── DOCUMENTATION ────
│   ├── configuration-flows.md    # Variables d'environnement, diagrammes de sequence
│   ├── CSS_STYLE_GUIDE.md        # Guide de style CSS (DaisyUI, conventions)
│   └── ANSSI_COMPLIANCE_REPORT.md # Rapport de conformite ANSSI
│
└── .cursor/                      ──── CONFIGURATION IDE ────
    └── commands/
        ├── speckit.analyze.md    # Commande Cursor : analyse de specification
        ├── speckit.plan.md       # Commande Cursor : planification
        ├── speckit.implement.md  # Commande Cursor : implementation
        └── (...)                 # Autres commandes speckit

Flux de demarrage

main.rs
  └─ RustwardenApp::builder()
       ├── .from_env()              → charge .env, init logging
       ├── .database_url(...)       → connexion PostgreSQL
       ├── .auto_migrate(true)      → execute les migrations SeaORM
       ├── .with_ui(default_routes) → enregistre les routes UI
       ├── .merge_routes(custom)    → ajoute les routes metier
       ├── .hooks(MyHooks)          → enregistre les hooks
       └── .build().await?
             ├── ensure_default_admin()
             ├── initialize_default_settings()
             ├── start_background_tasks()
             ├── init_i18n()
             └── assemble router + middleware

rustwarden-core en detail

Modules auth

FichierRole
auth/jwt.rsCreation et validation des JWT (HS256), struct Claims
auth/jwt_extractor.rsExtracteur Axum pour injecter Claims depuis le header/cookie
auth/session.rsConfiguration des sessions (memory, seaorm, redis)
auth/handlers.rsHandlers REST : login, register, refresh, logout, forgot/reset password, verify email
auth/extractors.rsAuthUser extracteur de haut niveau
auth/oauth_handlers.rsHandlers OAuth : redirect, callback, list_providers, list/unlink accounts (feature oauth)

Modules config

FichierRole
config/app.rsAppConfig + default_app_name() (lit APP_NAME depuis env)
config/auth.rsAuthConfig : JWT secret, expiration, refresh token
config/database.rsConnexion PostgreSQL via DATABASE_URL
config/email.rsEmailConfig : SMTP, queues, validation
config/server.rsServerConfig : host, port
config/admin.rsAdminConfig : identifiants admin initial
config/tasks.rsIntervalles des taches de fond
config/oauth.rsOAuthConfig : providers, client IDs, redirect URLs (feature oauth)

Modules models

15 entites SeaORM :

EntiteTableDescription
UserusersUtilisateurs (username, email, password hash, statut)
RolerolesRoles (admin, editor, viewer...)
PermissionpermissionsPermissions granulaires (resource.action)
UserRoleuser_rolesLiaison user ↔ role
RolePermissionrole_permissionsLiaison role ↔ permission
RefreshTokenrefresh_tokensTokens de rafraichissement JWT
JwtBlacklistjwt_blacklistJWT revoques
PasswordResetTokenpassword_reset_tokensTokens de reset mot de passe
PasswordChangeRequestpassword_change_requestsDemandes de changement (double confirmation)
EmailValidationTokenemail_validation_tokensTokens de validation email
EmailQueueemail_queueFile d'attente d'envoi d'emails
EmailDeliveryStatusemail_delivery_statusStatut de livraison email
AppSettingsapp_settingsParametres cle/valeur (app_name, app_domain, etc.)
AuditLogaudit_logsJournal d'audit des actions
UserPreferencesuser_preferencesPreferences utilisateur (theme, locale)
ResourceresourcesRegistre generique de ressources (type + ID + owner)
ResourceShareresource_sharesPartage d'une ressource avec un utilisateur
OauthAccountoauth_accountsComptes OAuth lies a un utilisateur (feature oauth)

Modules services

ServiceRole
UserServiceCRUD utilisateurs, recherche, pagination
AuthServiceLogique d'authentification (login, register, token generation)
RoleServiceCRUD roles, attribution aux utilisateurs
PermissionServiceCRUD permissions, verification user_has_permission
PasswordServiceHashing bcrypt, validation complexite ANSSI
PasswordResetServiceWorkflow reset par email
PasswordChangeServiceChangement avec confirmation double-etape
EmailServiceEnvoi SMTP via Lettre
EmailQueueServiceGestion de la file d'attente email
EmailValidationServiceEnvoi et verification des emails
JwtBlacklistServiceRevocation et cleanup des JWT
RefreshTokenServiceGestion des refresh tokens
ImapBounceServiceDetection de bounces par IMAP
AppSettingsServiceLecture/ecriture des parametres applicatifs
DefaultAdminServiceCreation de l'admin initial au demarrage
UserPreferencesServicePreferences theme/locale par utilisateur
AuthLoggingJournalisation des evenements d'authentification
EmailSanitizationNettoyage et validation des adresses email
ResourceServiceRegistre generique de ressources, partage, verification d'acces
OAuthServiceLiaison/deliaison comptes OAuth, find_or_create_user (feature oauth)
OAuthProvider (trait)Interface generique pour un provider OAuth/OIDC (feature oauth)
ProviderRegistryRegistre des providers actifs (Google, GitHub, Discord, Microsoft) (feature oauth)

rustwarden-ui en detail

Handlers

Chaque handler correspond a une ou plusieurs routes :

HandlerRoutes principales
root_handler/ (redirection)
login_handler/login, /resend-verification, /reset-password
home_handler/home
settings_handler/profile, /settings/password, /settings/sessions, /settings/preferences
admin_handler/admin, /admin/users/*, /admin/roles/*, /admin/settings
user_handler/api/v1/users
email_handlerGestion des emails dans l'UI
health/health
navigation_handlerConstruction de la navigation dynamique
dashboard_handlerDonnees du dashboard admin

Templates Askama

Les templates utilisent la syntaxe Jinja2 via Askama. Variables globales fournies par LayoutContext :

VariableSource
page_titleDefini par chaque handler
app_nameAPP_NAME env var (defaut: "My App")
app_versionCARGO_PKG_VERSION
csrf_tokenSession CSRF

Chaque template struct (#[derive(Template)]) dans les handlers ajoute ses propres variables.

DTOs

Les DTOs (dto/) structurent les donnees envoyees aux templates :

DTOUsage
NavigationDTOItems de navigation (sidebar/navbar)
HomeDTO / AccountStatDTOStats du dashboard
UserProfileDTODonnees profil
AdminDTODonnees admin (listes, formulaires)
SettingsDTODonnees des pages settings
AuthDTOFormulaires login/register
BreadcrumbDTOFil d'Ariane
SidebarDTOBarre laterale admin

Ajouter des routes

Route API simple

Ajoutez des routes dans le merge_routes() du builder :

use axum::{Router, routing::{get, post}, Json, extract::State};
use sea_orm::DatabaseConnection;
use serde::{Deserialize, Serialize};

fn my_routes() -> Router<DatabaseConnection> {
    Router::new()
        .route("/api/v1/products", get(list_products).post(create_product))
        .route("/api/v1/products/:id", get(get_product))
}

#[derive(Serialize)]
struct Product {
    id: i32,
    name: String,
}

async fn list_products(State(db): State<DatabaseConnection>) -> Json<Vec<Product>> {
    Json(vec![])
}

Route protegee par authentification

use rustwarden_core::auth::jwt::Claims;

async fn protected(claims: Claims) -> String {
    format!("Bonjour, utilisateur {}", claims.sub)
}

L'extracteur Claims echoue avec 401 si aucun JWT valide n'est present.

Route avec page HTML (template Askama)

use askama::Template;
use axum::extract::{Extension, State};
use sea_orm::DatabaseConnection;
use std::sync::Arc;
use rustwarden_core::auth::jwt::Claims;
use rustwarden_core::i18n::extractor::Locale;
use rustwarden_core::i18n::service::I18nService;
use rustwarden_core::templates::LayoutContext;

#[derive(Template)]
#[template(path = "my_page.html")]
struct MyPageTemplate {
    page_title: String,
    csrf_token: String,
    app_name: String,
    app_version: String,
    app_domain: String,
    app_description: String,
    username: String,
    email: String,
    initials: String,
    avatar_color: String,
    roles_text: String,
    nav_items: Vec<NavItem>,
    has_pending_password_change: bool,
    user_menu_profile: String,
    user_menu_logout: String,
    // Vos champs supplementaires
    items: Vec<String>,
}

Note : Pour les pages qui etendent base.html, les champs page_title, app_name, app_version, csrf_token sont obligatoires car utilises dans le layout de base.


Ajouter un modele SeaORM

1. Creer l'entite

Dans crates/rustwarden-core/src/models/, creez product.rs :

use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "products")]
pub struct Model {
    #[sea_orm(primary_key)]
    pub id: Uuid,
    pub name: String,
    #[sea_orm(column_type = "Decimal(Some((10, 2)))")]
    pub price: Decimal,
    pub created_by: Uuid,
    pub created_at: DateTimeWithTimeZone,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
    #[sea_orm(
        belongs_to = "super::user::Entity",
        from = "Column::CreatedBy",
        to = "super::user::Column::Id"
    )]
    User,
}

impl Related<super::user::Entity> for Entity {
    fn to() -> RelationDef {
        Relation::User.def()
    }
}

impl ActiveModelBehavior for ActiveModel {}

2. Declarer le module

Dans crates/rustwarden-core/src/models/mod.rs :

pub mod product;
pub use product::Entity as Product;

3. Creer la migration

Voir la section Ajouter une migration.


Ajouter une migration

1. Creer le fichier

Dans crates/rustwarden-core/migrations/src/, creez m20260310_000004_create_products_table.rs :

use sea_orm_migration::prelude::*;

#[derive(DeriveMigrationName)]
pub struct Migration;

#[async_trait::async_trait]
impl MigrationTrait for Migration {
    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
        manager
            .create_table(
                Table::create()
                    .table(Product::Table)
                    .if_not_exists()
                    .col(ColumnDef::new(Product::Id).uuid().not_null().primary_key())
                    .col(ColumnDef::new(Product::Name).string_len(255).not_null())
                    .col(ColumnDef::new(Product::Price).decimal_len(10, 2).not_null())
                    .col(ColumnDef::new(Product::CreatedBy).uuid().not_null())
                    .col(ColumnDef::new(Product::CreatedAt).timestamp_with_time_zone().not_null())
                    .foreign_key(
                        ForeignKey::create()
                            .from(Product::Table, Product::CreatedBy)
                            .to(Alias::new("users"), Alias::new("id"))
                            .on_delete(ForeignKeyAction::Cascade),
                    )
                    .to_owned(),
            )
            .await
    }

    async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
        manager
            .drop_table(Table::drop().table(Product::Table).to_owned())
            .await
    }
}

#[derive(Iden)]
enum Product {
    Table,
    Id,
    Name,
    Price,
    CreatedBy,
    CreatedAt,
}

2. Enregistrer la migration

Dans crates/rustwarden-core/src/migrations.rs, ajoutez :

#[path = "../migrations/src/m20260310_000004_create_products_table.rs"]
pub mod m20260310_000004_create_products_table;

Et dans la fonction core_migrations() :

pub fn core_migrations() -> Vec<Box<dyn MigrationTrait>> {
    vec![
        Box::new(m20260305_000001_initial_schema::Migration),
        Box::new(m20260306_000002_create_app_settings_table::Migration),
        Box::new(m20260309_000003_create_permissions_tables::Migration),
        Box::new(m20260310_000004_create_products_table::Migration),  // ← nouveau
    ]
}

Convention de nommage

Format : m{YYYYMMDD}_{NNNNNN}_{description}.rs

  • YYYYMMDD : date de creation
  • NNNNNN : numero sequentiel
  • description : nom court en snake_case

Ajouter un service

Dans crates/rustwarden-core/src/services/, creez product_service.rs :

use sea_orm::*;
use uuid::Uuid;
use crate::models::product;

pub struct ProductService;

impl ProductService {
    pub async fn find_all(db: &DatabaseConnection) -> Result<Vec<product::Model>, DbErr> {
        product::Entity::find()
            .order_by_desc(product::Column::CreatedAt)
            .all(db)
            .await
    }

    pub async fn find_by_id(db: &DatabaseConnection, id: Uuid) -> Result<Option<product::Model>, DbErr> {
        product::Entity::find_by_id(id).one(db).await
    }

    pub async fn create(
        db: &DatabaseConnection,
        name: String,
        price: Decimal,
        created_by: Uuid,
    ) -> Result<product::Model, DbErr> {
        let model = product::ActiveModel {
            id: Set(Uuid::new_v4()),
            name: Set(name),
            price: Set(price),
            created_by: Set(created_by),
            created_at: Set(chrono::Utc::now().into()),
        };
        model.insert(db).await
    }

    pub async fn update(
        db: &DatabaseConnection,
        id: Uuid,
        name: String,
        price: Decimal,
    ) -> Result<product::Model, DbErr> {
        let existing = product::Entity::find_by_id(id)
            .one(db)
            .await?
            .ok_or(DbErr::RecordNotFound("Product not found".into()))?;

        let mut active: product::ActiveModel = existing.into();
        active.name = Set(name);
        active.price = Set(price);
        active.update(db).await
    }

    pub async fn delete(db: &DatabaseConnection, id: Uuid) -> Result<DeleteResult, DbErr> {
        product::Entity::delete_by_id(id).exec(db).await
    }
}

Puis declarez-le dans crates/rustwarden-core/src/services/mod.rs :

pub mod product_service;

Personnaliser les templates

Emplacement

Les templates HTML sont dans crates/rustwarden-ui/templates/ et utilisent le moteur Askama (syntaxe Jinja2).

Fichiers principaux

FichierDescription
base.htmlLayout de base (head, scripts, theme, structure page)
components/navbar.htmlBarre de navigation avec menu utilisateur
components/header.htmlEn-tete avec logo et nom d'application
home.htmlPage d'accueil apres connexion
login.htmlPage de connexion et inscription
settings/profile.htmlEdition du profil utilisateur
settings/password.htmlChangement de mot de passe
settings/sessions.htmlGestion des sessions actives
settings/preferences.htmlPreferences (theme, langue)
admin/dashboard.htmlDashboard d'administration
admin/users_list.htmlListe des utilisateurs
admin/user_form.htmlFormulaire creation/edition utilisateur
admin/roles_list.htmlListe des roles
admin/role_form.htmlFormulaire creation/edition role
admin/settings.htmlParametres applicatifs
errors/401.htmlErreur non authentifie
errors/403.htmlErreur acces refuse
errors/500.htmlErreur serveur
email_verified.htmlPage de confirmation de verification email

Comment modifier

  1. Editez directement les fichiers .html
  2. Si vous ajoutez des variables, modifiez le struct #[derive(Template)] correspondant dans handlers/
  3. Recompilez : Askama verifie les templates a la compilation

Variables disponibles globalement

Toute page qui etend base.html a acces a :

{{ page_title }}     — Titre de la page
{{ app_name }}       — Nom de l'application (depuis APP_NAME)
{{ app_version }}    — Version du crate
{{ csrf_token }}     — Token CSRF pour les formulaires

Framework CSS

  • Tailwind CSS via CDN
  • DaisyUI pour les composants (boutons, cards, modals, etc.)
  • HTMX pour les requetes AJAX sans JavaScript
  • Alpine.js pour l'interactivite legere

Branding

Le nom de l'application est injecte via {{ app_name }} dans tous les templates. Pour changer le branding :

  1. Modifier APP_NAME dans .env
  2. Remplacer static/images/logo.svg et static/images/favicon.svg

Authentification et autorisation

Flux JWT

  1. Login (POST /api/v1/auth/login) → retourne access_token + refresh_token
  2. Requetes authentifiees → envoyer Authorization: Bearer <access_token> ou cookie token
  3. Refresh (POST /api/v1/auth/refresh) → nouveau access_token
  4. Logout (POST /api/v1/auth/logout) → le JWT est ajoute a la blacklist

Extracteur Claims

use rustwarden_core::auth::jwt::Claims;

async fn handler(claims: Claims) -> String {
    let user_id: Uuid = claims.user_id();
    let username: &str = &claims.sub;
    let roles: &[String] = &claims.roles;
    format!("User {} with roles {:?}", username, roles)
}

Middleware d'authentification

MiddlewareModuleEffet
AuthServicemiddleware/auth_service.rsExtrait le JWT, injecte Claims dans les extensions
auth_requiredmiddleware/auth_required.rsRetourne 401 si pas de JWT valide
admin_requiredmiddleware/admin_required.rsRetourne 403 si l'utilisateur n'a pas le role admin

Sessions

Le framework supporte 3 backends de session :

BackendVariableDescription
memorySESSION_BACKEND=memoryEn memoire (perdu au redemarrage)
seaormSESSION_BACKEND=seaormStocke en base PostgreSQL
redisSESSION_BACKEND=redisStocke dans Redis (REDIS_URL)

RBAC : roles et permissions

Structure de donnees

users ──N:M──> user_roles ──N:M──> roles ──N:M──> role_permissions ──N:M──> permissions

Services

use rustwarden_core::services::role_service::RoleService;
use rustwarden_core::services::permission_service::PermissionService;

// Roles d'un utilisateur
let roles = RoleService::get_user_roles(&db, user_id).await?;

// Verifier une permission
let can_edit = PermissionService::user_has_permission(&db, user_id, "products.edit").await?;

// Assigner un role
RoleService::assign_role_to_user(&db, user_id, role_id).await?;

Convention de nommage des permissions

Format recommande : {resource}.{action}

Exemples : users.read, users.create, users.update, users.delete, articles.publish, reports.export

Administration

Le panneau admin (/admin/roles) fournit un CRUD complet pour les roles et leurs permissions, sans code supplementaire.


Ressources partageables

Le framework fournit un systeme generique de registre de ressources avec partage entre utilisateurs. Toute entite applicative (document, projet, facture...) peut etre enregistree dans ce registre et partagee avec des niveaux de permission.

Concept

┌─────────────────────────────────┐
│  Votre application              │
│  tables : documents, projects   │
└──────────────┬──────────────────┘
               │ ResourceService::register()
┌──────────────▼──────────────────┐
│  rustwarden-core (framework)    │
│  tables : resources,            │
│           resource_shares       │
│  → gere propriete, partage,     │
│    verification d'acces         │
└─────────────────────────────────┘

Le framework ne connait pas vos tables applicatives. Il stocke un triplet (resource_type, resource_id, owner_id) et gere le partage par-dessus.

Niveaux de permission

NiveauInclutDescription
read—Lecture seule
writereadLecture + ecriture
adminread, writeTout + gestion des partages
ownertoutProprietaire (implicite, pas stocke)

La hierarchie est automatique : un partage write accorde aussi read.

Exemple complet : ressource "Document"

1. Creer le modele applicatif

// Dans votre application : src/models/document.rs
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, Serialize, Deserialize, DeriveEntityModel)]
#[sea_orm(table_name = "documents")]
pub struct Model {
    #[sea_orm(primary_key)]
    pub id: Uuid,
    pub title: String,
    pub content: String,
    pub created_by: Uuid,
    pub created_at: DateTimeWithTimeZone,
    pub updated_at: DateTimeWithTimeZone,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

2. Enregistrer la ressource a la creation

use rustwarden_core::services::resource_service::ResourceService;

pub async fn create_document(
    db: &DatabaseConnection,
    title: String,
    content: String,
    user_id: Uuid,
) -> Result<document::Model, AppError> {
    // 1. Creer le document dans votre table
    let doc = document::ActiveModel {
        id: Set(Uuid::new_v4()),
        title: Set(title),
        content: Set(content),
        created_by: Set(user_id),
        created_at: Set(chrono::Utc::now().into()),
        updated_at: Set(chrono::Utc::now().into()),
    };
    let doc = doc.insert(db).await?;

    // 2. Enregistrer dans le registre generique du framework
    ResourceService::register(db, "document", doc.id, user_id).await?;

    Ok(doc)
}

3. Partager un document

// Partager en lecture avec un autre utilisateur
ResourceService::share(
    db,
    "document",         // type de ressource
    doc_id,             // ID du document
    other_user_id,      // destinataire du partage
    "read",             // niveau de permission
    current_user_id,    // qui partage
).await?;

// Partager en ecriture
ResourceService::share(db, "document", doc_id, collaborator_id, "write", current_user_id).await?;

4. Verifier l'acces dans un handler

use rustwarden_core::services::resource_service::ResourceService;
use rustwarden_core::auth::jwt::Claims;

async fn get_document(
    claims: Claims,
    Path(doc_id): Path<Uuid>,
    State(db): State<DatabaseConnection>,
) -> Result<Json<document::Model>, StatusCode> {
    // Verifier que l'utilisateur a au moins le droit "read"
    let can_read = ResourceService::user_can_access(
        &db, claims.user_id(), "document", doc_id, "read"
    ).await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

    if !can_read {
        return Err(StatusCode::FORBIDDEN);
    }

    // Charger le document
    let doc = document::Entity::find_by_id(doc_id)
        .one(&db).await
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
        .ok_or(StatusCode::NOT_FOUND)?;

    Ok(Json(doc))
}

async fn update_document(
    claims: Claims,
    Path(doc_id): Path<Uuid>,
    State(db): State<DatabaseConnection>,
    Json(payload): Json<UpdateDocumentPayload>,
) -> Result<Json<document::Model>, StatusCode> {
    // Verifier le droit "write"
    let can_write = ResourceService::user_can_access(
        &db, claims.user_id(), "document", doc_id, "write"
    ).await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

    if !can_write {
        return Err(StatusCode::FORBIDDEN);
    }

    // Mettre a jour...
    todo!()
}

5. Lister les documents accessibles

// Tous les documents que l'utilisateur peut voir (proprietaire + partages + publics)
let accessible = ResourceService::accessible_by_user(&db, user_id, "document").await?;

// Seulement ceux partages avec l'utilisateur (avec le niveau de permission)
let shared = ResourceService::shared_with_user(&db, user_id, "document").await?;
for (resource, share) in &shared {
    println!("Document {} — permission: {}", resource.resource_id, share.permission_level);
}

// Seulement ceux que l'utilisateur possede
let owned = ResourceService::owned_by_user(&db, user_id, "document").await?;

6. Gerer les partages d'un document

// Lister les partages existants
let shares = ResourceService::list_shares(&db, "document", doc_id).await?;

// Revoquer l'acces d'un utilisateur
ResourceService::revoke_user_access(&db, "document", doc_id, user_id).await?;

// Rendre un document public (lisible par tous les utilisateurs authentifies)
ResourceService::set_public(&db, "document", doc_id, true).await?;

// Niveau de permission effectif d'un utilisateur
let level = ResourceService::effective_permission(&db, user_id, "document", doc_id).await?;
// -> Some("owner"), Some("write"), Some("read"), ou None

7. Supprimer un document

pub async fn delete_document(db: &DatabaseConnection, doc_id: Uuid) -> Result<(), AppError> {
    // 1. Supprimer du registre (cascade automatique vers resource_shares)
    ResourceService::unregister(db, "document", doc_id).await?;

    // 2. Supprimer le document de votre table
    document::Entity::delete_by_id(doc_id).exec(db).await?;

    Ok(())
}

Hooks de partage

Interceptez les evenements de partage dans vos hooks :

#[async_trait]
impl RustwardenHooks for MyHooks {
    async fn on_resource_shared(
        &self,
        db: &DatabaseConnection,
        resource_type: &str,
        resource_id: Uuid,
        shared_with_user_id: Uuid,
        permission_level: &str,
        shared_by_user_id: Uuid,
    ) -> anyhow::Result<()> {
        // Envoyer une notification, creer une entree d'audit, etc.
        tracing::info!(
            "{} partage avec {} ({})",
            resource_type, shared_with_user_id, permission_level
        );
        Ok(())
    }

    async fn on_resource_unshared(
        &self,
        db: &DatabaseConnection,
        resource_type: &str,
        resource_id: Uuid,
        user_id: Uuid,
    ) -> anyhow::Result<()> {
        // Notifier l'utilisateur que son acces a ete revoque
        Ok(())
    }
}

API ResourceService — reference

MethodeDescription
register(db, type, id, owner)Enregistrer une ressource dans le registre
unregister(db, type, id)Retirer du registre (cascade les partages)
share(db, type, id, user, level, by)Partager avec un utilisateur
revoke_share(db, share_id)Revoquer un partage par ID
revoke_user_access(db, type, id, user)Revoquer tout acces d'un utilisateur
list_shares(db, type, id)Lister les partages d'une ressource
user_can_access(db, user, type, id, level)Verifier l'acces (bool)
effective_permission(db, user, type, id)Niveau effectif (owner/admin/write/read/None)
shared_with_user(db, user, type)Ressources partagees avec un utilisateur
accessible_by_user(db, user, type)Toutes les ressources accessibles
owned_by_user(db, user, type)Ressources possedees
set_public(db, type, id, bool)Rendre public/prive
find_by_type_and_id(db, type, id)Chercher dans le registre

Authentification OAuth / SSO

Le framework supporte l'authentification via OAuth2 et OpenID Connect. Cette fonctionnalite est compilee conditionnellement via le feature flag oauth.

Activation

Dans le Cargo.toml de votre application :

rustwarden-core = { path = "../crates/rustwarden-core", features = ["oauth"] }

Variables d'environnement

VariableDescriptionDefaut
OAUTH_ENCRYPTION_KEYCle AES-256-GCM (hex 64 chars) pour chiffrer les secrets OAuth en DB-
OAUTH_ENABLEDActive globalement OAuthfalse
OAUTH_REDIRECT_BASE_URLURL de base pour les callbackshttp://localhost:3000
OAUTH_GOOGLE_CLIENT_IDGoogle OAuth Client ID-
OAUTH_GOOGLE_CLIENT_SECRETGoogle OAuth Client Secret-
OAUTH_GOOGLE_ENABLEDActive Googlefalse
OAUTH_GITHUB_CLIENT_IDGitHub OAuth Client ID-
OAUTH_GITHUB_CLIENT_SECRETGitHub OAuth Client Secret-
OAUTH_GITHUB_ENABLEDActive GitHubfalse
OAUTH_DISCORD_CLIENT_IDDiscord OAuth Client ID-
OAUTH_DISCORD_CLIENT_SECRETDiscord OAuth Client Secret-
OAUTH_DISCORD_ENABLEDActive Discordfalse
OAUTH_MICROSOFT_CLIENT_IDMicrosoft OAuth Client ID-
OAUTH_MICROSOFT_CLIENT_SECRETMicrosoft OAuth Client Secret-
OAUTH_MICROSOFT_ENABLEDActive Microsoftfalse
OAUTH_MICROSOFT_TENANTAzure AD tenantcommon

Note : Les variables d'environnement sont utilises en fallback. La configuration principale se fait via Admin > Settings > OAuth / SSO dans l'interface web. Les Client Secrets sont chiffres (AES-256-GCM) en base de donnees. La cle de chiffrement OAUTH_ENCRYPTION_KEY doit etre definie pour pouvoir enregistrer des secrets via l'interface admin. Generer avec openssl rand -hex 32.

Configuration via Admin UI

L'interface d'administration (/admin/settings) permet de configurer OAuth sans redemarrer l'application pour les toggles (enable/disable), mais les modifications de credentials (Client ID, Client Secret, Tenant) necessitent un redemarrage.

L'ecran admin permet de :

  • Activer/desactiver OAuth globalement
  • Configurer oauth_redirect_base_url, oauth_auto_register, oauth_link_existing_account
  • Pour chaque provider : activer/desactiver, saisir Client ID et Client Secret
  • Afficher l'URI de redirection a enregistrer chez le provider
  • Voir le statut de configuration de chaque secret (configure / non configure)

Flux OAuth

  1. L'utilisateur clique sur "Se connecter avec Google" sur /login
  2. Le handler oauth_redirect genere une URL d'autorisation avec un state CSRF, un nonce OIDC et un pkce_verifier, les stocke en session, puis redirige vers le provider
  3. L'utilisateur s'authentifie chez le provider
  4. Le provider redirige vers /api/v1/auth/oauth/{provider}/callback avec un code et le state
  5. Le handler oauth_callback :
    • Valide le state en comparaison constant-time
    • Echange le code contre un token via PKCE
    • Extrait les informations utilisateur (email, nom, avatar)
    • Appelle OAuthService::find_or_create_user pour lier ou creer l'utilisateur
    • Genere un JWT + refresh token
    • Regenere le session ID (anti-fixation)
    • Redirige vers /home

Routes OAuth

MethodeRouteDescription
GET/api/v1/auth/oauth/{provider}/redirectRedirige vers le provider
GET/api/v1/auth/oauth/{provider}/callbackTraite le retour du provider
GET/api/v1/auth/oauth/providersListe les providers actifs (JSON)
GET/api/v1/auth/oauth/accountsListe les comptes OAuth de l'utilisateur
DELETE/api/v1/auth/oauth/accounts/{id}Delie un compte OAuth

Providers integres

ProviderProtocoleScopesPKCE
GoogleOpenID Connectopenid email profileS256
GitHubOAuth2 + API RESTuser:emailNon
DiscordOAuth2 + API RESTidentify emailNon
MicrosoftOpenID Connectopenid email profileS256

Ajouter un provider custom

Implementez le trait OAuthProvider :

use rustwarden_core::services::oauth::{OAuthProvider, ProviderUserInfo};
use rustwarden_core::services::oauth::provider_trait::AuthorizationUrl;
use async_trait::async_trait;

pub struct MyProvider {
    client_id: String,
    client_secret: String,
}

#[async_trait]
impl OAuthProvider for MyProvider {
    fn name(&self) -> &str { "my_provider" }
    fn display_name(&self) -> &str { "My Provider" }

    async fn authorization_url(&self, redirect_uri: &str) -> anyhow::Result<AuthorizationUrl> {
        // Construire l'URL d'autorisation
        todo!()
    }

    async fn exchange_code(
        &self,
        code: &str,
        redirect_uri: &str,
        pkce_verifier: Option<&str>,
        nonce: Option<&str>,
    ) -> anyhow::Result<ProviderUserInfo> {
        // Echanger le code et recuperer les infos utilisateur
        todo!()
    }
}

Puis enregistrez-le dans le ProviderRegistry (dans app.rs ou votre main.rs).

Securite OAuth

  • CSRF : Parametre state genere aleatoirement, stocke en session, valide en comparaison constant-time (subtle::ConstantTimeEq)
  • PKCE : Proof Key for Code Exchange (S256) pour les providers OIDC (Google, Microsoft)
  • Nonce OIDC : Genere et verifie dans l'ID token pour prevenir les attaques de replay
  • Email verifie requis : OAuthService::find_or_create_user refuse les comptes sans email verifie par le provider
  • Rate limiting : Le callback est protege par check_auth_rate_limit (10 req/min par IP)
  • Session fixation : session.cycle_id() apres chaque login OAuth reussi
  • Deliaison securisee : Impossible de supprimer le dernier moyen d'authentification (si pas de mot de passe)

Hooks : intercepter les evenements

Implementer le trait

use rustwarden_core::hooks::RustwardenHooks;
use async_trait::async_trait;
use sea_orm::DatabaseConnection;
use uuid::Uuid;

struct MyHooks;

#[async_trait]
impl RustwardenHooks for MyHooks {
    async fn on_user_registered(
        &self,
        db: &DatabaseConnection,
        user_id: Uuid,
        username: &str,
    ) -> anyhow::Result<()> {
        // Creer des donnees initiales pour le nouveau utilisateur
        Ok(())
    }

    async fn on_user_login(
        &self,
        db: &DatabaseConnection,
        user_id: Uuid,
        username: &str,
    ) -> anyhow::Result<()> {
        // Mettre a jour des statistiques, notifier, etc.
        Ok(())
    }
}

Activer les hooks

RustwardenApp::builder()
    .from_env()
    .hooks(MyHooks)
    .build().await?
    .run().await

Hooks disponibles

HookSignatureDeclencheur
on_user_registered(db, user_id, username)Apres inscription reussie
on_user_login(db, user_id, username)Apres login reussi
on_user_logout(db, user_id)Apres deconnexion
on_user_deleted(db, user_id)Apres suppression d'un utilisateur
on_password_changed(db, user_id)Apres changement de mot de passe
on_email_verified(db, user_id)Apres verification email
on_role_changed(db, user_id, roles)Apres modification des roles
on_oauth_login(db, user_id, provider, is_new_user)Apres login OAuth reussi
on_totp_enabled(db, user_id)Apres activation 2FA/TOTP
on_totp_disabled(db, user_id)Apres desactivation 2FA/TOTP
on_resource_registered(db, type, id, owner)Apres enregistrement d'une ressource
on_resource_unregistered(db, type, id)Apres desenregistrement
on_resource_shared(db, type, id, user, level, by)Apres partage d'une ressource
on_resource_unshared(db, type, id, user)Apres revocation d'un partage
on_team_created(db, team_id, creator_id)Apres creation d'une team
on_team_deleted(db, team_id)Apres suppression d'une team
on_team_member_added(db, team_id, user_id, role)Apres ajout d'un membre
on_team_member_removed(db, team_id, user_id)Apres retrait d'un membre
on_audit_event(db, event)Tout evenement audit (auth, admin, acces...)

Tous les hooks ont une implementation par defaut (no-op), implementez uniquement ceux dont vous avez besoin.

Exemple : hooks teams pour un audit log

use async_trait::async_trait;
use rustwarden_core::hooks::RustwardenHooks;
use sea_orm::DatabaseConnection;
use uuid::Uuid;

struct AuditHooks;

#[async_trait]
impl RustwardenHooks for AuditHooks {
    async fn on_team_created(
        &self,
        _db: &DatabaseConnection,
        team_id: Uuid,
        creator_id: Uuid,
    ) -> anyhow::Result<()> {
        tracing::info!(%team_id, %creator_id, "team created");
        // Ici : insert dans votre table audit_log métier
        Ok(())
    }

    async fn on_team_member_added(
        &self,
        _db: &DatabaseConnection,
        team_id: Uuid,
        user_id: Uuid,
        role: &str,
    ) -> anyhow::Result<()> {
        tracing::info!(%team_id, %user_id, role, "team member added");
        Ok(())
    }

    async fn on_team_member_removed(
        &self,
        _db: &DatabaseConnection,
        team_id: Uuid,
        user_id: Uuid,
    ) -> anyhow::Result<()> {
        tracing::info!(%team_id, %user_id, "team member removed");
        Ok(())
    }
}

Signatures canoniques dans crates/rustwarden-core/src/hooks.rs. Voir aussi docs/SERVICES_REFERENCE.md#17-hooks-lifecycle-et-audit pour la reference complete des parametres.


Internationalisation (i18n)

Fichiers de traduction

Format YAML dans locales/ :

# locales/fr.yml
auth:
  login_title: "Connexion"
  register_title: "Inscription"
products:
  title: "Produits"
  empty: "Aucun produit"

Utilisation dans un handler

use rustwarden_core::i18n::extractor::Locale;
use rustwarden_core::i18n::service::I18nService;
use std::sync::Arc;
use axum::extract::Extension;

async fn page(
    locale: Locale,
    Extension(i18n): Extension<Arc<I18nService>>,
) -> String {
    let title = i18n.t(&locale.0, "products.title");
    title
}

Detection de la locale

Ordre de priorite :

  1. Cookie locale (defini par les preferences utilisateur)
  2. Header HTTP Accept-Language
  3. Variable d'environnement DEFAULT_LOCALE

Ajouter une langue

  1. Creer locales/pt.yml avec les traductions
  2. Ajouter "pt" dans .supported_locales(vec!["fr", "en", "de", "es", "pt"])
  3. Ajouter les labels dans l'UI preferences pour que les utilisateurs puissent la selectionner

Taches de fond

Les taches de fond demarrent automatiquement avec le builder (sauf si .without_background_tasks()).

TacheModuleDescription
Email queue processortasks/email_queue_processor.rsEnvoie les emails en attente par batch
Password change email retrytasks/password_change_email_retry.rsRelance les confirmations de changement de mot de passe
Password change cleanuptasks/password_change_cleanup.rsNettoie les demandes expirees
JWT blacklist cleanupdans app.rsSupprime les JWT expires de la blacklist
IMAP bounce processorservices/imap_bounce_service.rsVerifie les bounces email par IMAP

Nettoyage des Personal Access Tokens

Le framework fournit PersonalTokenCleanupProcessor<S: TokenScope> qui purge periodiquement les PAT expires et revoques. Contrairement aux taches ci-dessus, il n'est pas demarre automatiquement : il est generique sur le scope S (enum metier defini par votre app), et le framework ne peut pas connaitre ce type a la compilation.

Dans votre main.rs, apres avoir defini votre enum Scope et son implementation de TokenScope, demarrez la tache juste avant run() :

use rustwarden_core::prelude::{PersonalTokenCleanupProcessor, RustwardenApp, TokenScope};
use sea_orm::DatabaseConnection;
use std::sync::Arc;

#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
enum MyScope { ApiRead, ApiWrite, Admin }

impl TokenScope for MyScope {
    fn as_str(&self) -> &'static str {
        match self {
            Self::ApiRead => "api:read",
            Self::ApiWrite => "api:write",
            Self::Admin => "admin",
        }
    }
    fn parse(raw: &str) -> Option<Self> {
        match raw {
            "api:read" => Some(Self::ApiRead),
            "api:write" => Some(Self::ApiWrite),
            "admin" => Some(Self::Admin),
            _ => None,
        }
    }
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let app = RustwardenApp::builder().from_env().build().await?;
    let db_arc: Arc<DatabaseConnection> = Arc::new(app.db().clone());
    PersonalTokenCleanupProcessor::<MyScope>::new(db_arc).start();
    app.run().await
}

Intervalle configurable via PERSONAL_TOKEN_CLEANUP_INTERVAL_SECS (defaut dans config::tasks::personal_token_cleanup_interval_secs).

Voir aussi docs/SERVICES_REFERENCE.md#7-tokens-et-sessions pour l'API complete de PersonalTokenService<S>.

Ajouter une tache de fond

let db_clone = db.clone();
tokio::spawn(async move {
    let mut interval = tokio::time::interval(Duration::from_secs(60));
    loop {
        interval.tick().await;
        // Votre logique ici
        my_cleanup_task(&db_clone).await;
    }
});

Rate limiting et notifications temps-reel

RateLimitService

Sliding-window in-process generique. Utilise par le framework (reset mot de passe, validation email, changement mot de passe) et disponible dans votre app.

use rustwarden_core::prelude::RateLimitService;

let limiter = RateLimitService::new();
// 5 tentatives max sur une fenetre glissante de 60s, par cle arbitraire
match limiter.check_sliding_window("login:jdoe", 5, 60) {
    Ok(()) => { /* requete autorisee */ }
    Err(retry_after) => { /* trop de tentatives, retry apres `retry_after` secondes */ }
}

Voir docs/SERVICES_REFERENCE.md#15-rate-limiting pour les options avancees (double map, decisions atomiques, TTL).

Notifications temps-reel (SSE)

NotificationBus<T> est un pub/sub generique avec filtre par subscriber, backé par tokio::sync::broadcast. sse_response emballe la subscription en reponse Server-Sent Events avec keep-alive 30s.

use axum::{extract::Extension, response::IntoResponse};
use rustwarden_core::prelude::{InMemoryBus, NotificationBus, sse_response};
use std::sync::Arc;

#[derive(Clone, serde::Serialize)]
enum AppEvent {
    ImportProgress { job_id: uuid::Uuid, pct: u8 },
    Ping,
}

// Dans le builder de l'app, injecter le bus comme Extension :
//   let bus: Arc<dyn NotificationBus<AppEvent>> = Arc::new(InMemoryBus::new(128));
//   .with_extension(bus.clone())

async fn stream(
    Extension(bus): Extension<Arc<dyn NotificationBus<AppEvent>>>,
) -> impl IntoResponse {
    let filter = Arc::new(|e: &AppEvent| matches!(e, AppEvent::ImportProgress { .. }));
    sse_response(bus.subscribe(filter))
}

// Coté producteur :
// bus.publish(AppEvent::ImportProgress { job_id, pct: 42 }).await;

Limitation : InMemoryBus est single-process. Pour le multi-instance (horizontal scaling), il faudra un backend Redis (trait stable, backend remplacable sans changer les handlers). Voir docs/SERVICES_REFERENCE.md#16-notifications-temps-reel-et-sse.


Middleware

Middleware disponibles

MiddlewareFichierDescription
AuthServicemiddleware/auth_service.rsExtraction JWT, injection Claims
AdminServicemiddleware/admin_service.rsVerification role admin
auth_requiredmiddleware/auth_required.rsProtection route : 401 sans auth
admin_requiredmiddleware/admin_required.rsProtection route : 403 sans admin
csrfmiddleware/csrf.rsProtection CSRF (token dans session)
rate_limitmiddleware/rate_limit.rsLimitation du debit par IP
error_handlermiddleware/error_handler.rsCapture et formate les erreurs
password_complexitymiddleware/password_complexity.rsValidation ANSSI des mots de passe
update_last_loginmiddleware/update_last_login.rsMet a jour last_login_at
admin_extractormiddleware/admin_extractor.rsExtracteur pour les pages admin
admin_helpermiddleware/admin_helper.rsFonctions d'aide pour l'admin

Ajouter un middleware Axum

use axum::{middleware, Router};

async fn my_middleware(
    request: axum::extract::Request,
    next: axum::middleware::Next,
) -> axum::response::Response {
    // Logique avant le handler
    let response = next.run(request).await;
    // Logique apres le handler
    response
}

fn my_routes() -> Router<DatabaseConnection> {
    Router::new()
        .route("/protected", get(handler))
        .layer(middleware::from_fn(my_middleware))
}

Configuration

Charger la configuration depuis l'environnement

Tous les modules de config utilisent le pattern ::from_env() :

use rustwarden_core::config::auth::AuthConfig;
use rustwarden_core::config::server::ServerConfig;
use rustwarden_core::config::email::EmailConfig;
use rustwarden_core::config::app::AppConfig;

let auth = AuthConfig::from_env()?;
let server = ServerConfig::from_env();
let email = EmailConfig::from_env()?;
let app = AppConfig::from_env();

APP_NAME

La variable APP_NAME est lue par config::app::default_app_name() et utilisee :

  • Dans LayoutContext (toutes les pages HTML)
  • Dans les emails (nom d'expediteur si SMTP_FROM_NAME n'est pas defini)
  • Dans AppConfig

Fallback : "My App" si APP_NAME n'est pas defini.

app_settings (base de donnees)

En plus des variables d'environnement, certains parametres sont stockes en base via AppSettingsService :

CleDescription
app_nameNom de l'application (peut etre modifie via /admin/settings)
app_domainDomaine de l'application
app_descriptionDescription pour les meta tags

Les settings DB prennent priorite sur l'env pour l'affichage dans l'UI.


Commandes binaires

BinaireFichierUsage
migratecore/bin/Execute les migrations : cargo run --bin migrate
seedcore/bin/seed.rsCree l'admin initial depuis les variables d'env
hash_passwordcore/bin/hash_password.rsHashe un mot de passe bcrypt en ligne de commande
change_passwordcore/bin/change_password.rsChange le mot de passe d'un utilisateur

Le CLI wizard

Le wizard rustwarden-cli genere des projets autonomes. Voir la documentation detaillee dans tools/rustwarden-cli/README.md.

cargo run -p rustwarden-cli -- start-project

Le projet genere inclut un DEVELOPER.md adapte avec les instructions specifiques au projet.


Tests

# Tous les tests
cargo test

# Tests d'un crate specifique
cargo test -p rustwarden-core
cargo test -p rustwarden-ui

# Tests avec output
cargo test -- --nocapture

Production

Checklist avant deploiement

  • APP_DEBUG=false
  • COOKIE_SECURE=true (si HTTPS)
  • JWT_SECRET : cle unique de 64+ caracteres
  • ADMIN_PASSWORD : mot de passe fort
  • SMTP_USE_TLS=true
  • SMTP_ACCEPT_INVALID_CERTS=false
  • ALLOW_REGISTRATION=false (sauf si necessaire)
  • RUST_LOG=info (pas debug)
  • SESSION_BACKEND=seaorm ou redis (pas memory)
  • Si OAuth active : OAUTH_ENCRYPTION_KEY est definie (64 caracteres hex)
  • Si OAuth active : OAUTH_REDIRECT_BASE_URL pointe vers le domaine HTTPS de production
  • Si OAuth active : les Client ID/Secret de chaque provider sont configures (via Admin UI ou env vars)
  • Si OAuth active : les redirect URIs sont enregistrees dans la console de chaque provider

Build optimise

cargo build --release

Variables d'environnement

En production, preferez les variables d'environnement systeme plutot que le fichier .env.