Guide du developpeur — Rustwarden
Ce document est le guide de reference pour comprendre, etendre et contribuer au framework Rustwarden.
Table des matieres
- Architecture globale
- rustwarden-core en detail
- rustwarden-ui en detail
- Ajouter des routes
- Ajouter un modele SeaORM
- Ajouter une migration
- Ajouter un service
- Personnaliser les templates
- Authentification et autorisation
- RBAC : roles et permissions
- Authentification OAuth / SSO
- Hooks : intercepter les evenements
- Internationalisation (i18n)
- Taches de fond
- Rate limiting et notifications temps-reel
- Middleware
- Configuration
- Commandes binaires
- Le CLI wizard
- Tests
- 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
| Fichier | Role |
|---|---|
auth/jwt.rs | Creation et validation des JWT (HS256), struct Claims |
auth/jwt_extractor.rs | Extracteur Axum pour injecter Claims depuis le header/cookie |
auth/session.rs | Configuration des sessions (memory, seaorm, redis) |
auth/handlers.rs | Handlers REST : login, register, refresh, logout, forgot/reset password, verify email |
auth/extractors.rs | AuthUser extracteur de haut niveau |
auth/oauth_handlers.rs | Handlers OAuth : redirect, callback, list_providers, list/unlink accounts (feature oauth) |
Modules config
| Fichier | Role |
|---|---|
config/app.rs | AppConfig + default_app_name() (lit APP_NAME depuis env) |
config/auth.rs | AuthConfig : JWT secret, expiration, refresh token |
config/database.rs | Connexion PostgreSQL via DATABASE_URL |
config/email.rs | EmailConfig : SMTP, queues, validation |
config/server.rs | ServerConfig : host, port |
config/admin.rs | AdminConfig : identifiants admin initial |
config/tasks.rs | Intervalles des taches de fond |
config/oauth.rs | OAuthConfig : providers, client IDs, redirect URLs (feature oauth) |
Modules models
15 entites SeaORM :
| Entite | Table | Description |
|---|---|---|
User | users | Utilisateurs (username, email, password hash, statut) |
Role | roles | Roles (admin, editor, viewer...) |
Permission | permissions | Permissions granulaires (resource.action) |
UserRole | user_roles | Liaison user ↔ role |
RolePermission | role_permissions | Liaison role ↔ permission |
RefreshToken | refresh_tokens | Tokens de rafraichissement JWT |
JwtBlacklist | jwt_blacklist | JWT revoques |
PasswordResetToken | password_reset_tokens | Tokens de reset mot de passe |
PasswordChangeRequest | password_change_requests | Demandes de changement (double confirmation) |
EmailValidationToken | email_validation_tokens | Tokens de validation email |
EmailQueue | email_queue | File d'attente d'envoi d'emails |
EmailDeliveryStatus | email_delivery_status | Statut de livraison email |
AppSettings | app_settings | Parametres cle/valeur (app_name, app_domain, etc.) |
AuditLog | audit_logs | Journal d'audit des actions |
UserPreferences | user_preferences | Preferences utilisateur (theme, locale) |
Resource | resources | Registre generique de ressources (type + ID + owner) |
ResourceShare | resource_shares | Partage d'une ressource avec un utilisateur |
OauthAccount | oauth_accounts | Comptes OAuth lies a un utilisateur (feature oauth) |
Modules services
| Service | Role |
|---|---|
UserService | CRUD utilisateurs, recherche, pagination |
AuthService | Logique d'authentification (login, register, token generation) |
RoleService | CRUD roles, attribution aux utilisateurs |
PermissionService | CRUD permissions, verification user_has_permission |
PasswordService | Hashing bcrypt, validation complexite ANSSI |
PasswordResetService | Workflow reset par email |
PasswordChangeService | Changement avec confirmation double-etape |
EmailService | Envoi SMTP via Lettre |
EmailQueueService | Gestion de la file d'attente email |
EmailValidationService | Envoi et verification des emails |
JwtBlacklistService | Revocation et cleanup des JWT |
RefreshTokenService | Gestion des refresh tokens |
ImapBounceService | Detection de bounces par IMAP |
AppSettingsService | Lecture/ecriture des parametres applicatifs |
DefaultAdminService | Creation de l'admin initial au demarrage |
UserPreferencesService | Preferences theme/locale par utilisateur |
AuthLogging | Journalisation des evenements d'authentification |
EmailSanitization | Nettoyage et validation des adresses email |
ResourceService | Registre generique de ressources, partage, verification d'acces |
OAuthService | Liaison/deliaison comptes OAuth, find_or_create_user (feature oauth) |
OAuthProvider (trait) | Interface generique pour un provider OAuth/OIDC (feature oauth) |
ProviderRegistry | Registre des providers actifs (Google, GitHub, Discord, Microsoft) (feature oauth) |
rustwarden-ui en detail
Handlers
Chaque handler correspond a une ou plusieurs routes :
| Handler | Routes 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_handler | Gestion des emails dans l'UI |
health | /health |
navigation_handler | Construction de la navigation dynamique |
dashboard_handler | Donnees du dashboard admin |
Templates Askama
Les templates utilisent la syntaxe Jinja2 via Askama. Variables globales fournies par LayoutContext :
| Variable | Source |
|---|---|
page_title | Defini par chaque handler |
app_name | APP_NAME env var (defaut: "My App") |
app_version | CARGO_PKG_VERSION |
csrf_token | Session CSRF |
Chaque template struct (#[derive(Template)]) dans les handlers ajoute ses propres variables.
DTOs
Les DTOs (dto/) structurent les donnees envoyees aux templates :
| DTO | Usage |
|---|---|
NavigationDTO | Items de navigation (sidebar/navbar) |
HomeDTO / AccountStatDTO | Stats du dashboard |
UserProfileDTO | Donnees profil |
AdminDTO | Donnees admin (listes, formulaires) |
SettingsDTO | Donnees des pages settings |
AuthDTO | Formulaires login/register |
BreadcrumbDTO | Fil d'Ariane |
SidebarDTO | Barre 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 champspage_title,app_name,app_version,csrf_tokensont 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 creationNNNNNN: numero sequentieldescription: 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
| Fichier | Description |
|---|---|
base.html | Layout de base (head, scripts, theme, structure page) |
components/navbar.html | Barre de navigation avec menu utilisateur |
components/header.html | En-tete avec logo et nom d'application |
home.html | Page d'accueil apres connexion |
login.html | Page de connexion et inscription |
settings/profile.html | Edition du profil utilisateur |
settings/password.html | Changement de mot de passe |
settings/sessions.html | Gestion des sessions actives |
settings/preferences.html | Preferences (theme, langue) |
admin/dashboard.html | Dashboard d'administration |
admin/users_list.html | Liste des utilisateurs |
admin/user_form.html | Formulaire creation/edition utilisateur |
admin/roles_list.html | Liste des roles |
admin/role_form.html | Formulaire creation/edition role |
admin/settings.html | Parametres applicatifs |
errors/401.html | Erreur non authentifie |
errors/403.html | Erreur acces refuse |
errors/500.html | Erreur serveur |
email_verified.html | Page de confirmation de verification email |
Comment modifier
- Editez directement les fichiers
.html - Si vous ajoutez des variables, modifiez le struct
#[derive(Template)]correspondant danshandlers/ - 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 :
- Modifier
APP_NAMEdans.env - Remplacer
static/images/logo.svgetstatic/images/favicon.svg
Authentification et autorisation
Flux JWT
- Login (
POST /api/v1/auth/login) → retourneaccess_token+refresh_token - Requetes authentifiees → envoyer
Authorization: Bearer <access_token>ou cookietoken - Refresh (
POST /api/v1/auth/refresh) → nouveauaccess_token - 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
| Middleware | Module | Effet |
|---|---|---|
AuthService | middleware/auth_service.rs | Extrait le JWT, injecte Claims dans les extensions |
auth_required | middleware/auth_required.rs | Retourne 401 si pas de JWT valide |
admin_required | middleware/admin_required.rs | Retourne 403 si l'utilisateur n'a pas le role admin |
Sessions
Le framework supporte 3 backends de session :
| Backend | Variable | Description |
|---|---|---|
memory | SESSION_BACKEND=memory | En memoire (perdu au redemarrage) |
seaorm | SESSION_BACKEND=seaorm | Stocke en base PostgreSQL |
redis | SESSION_BACKEND=redis | Stocke 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
| Niveau | Inclut | Description |
|---|---|---|
read | — | Lecture seule |
write | read | Lecture + ecriture |
admin | read, write | Tout + gestion des partages |
owner | tout | Proprietaire (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
| Methode | Description |
|---|---|
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
| Variable | Description | Defaut |
|---|---|---|
OAUTH_ENCRYPTION_KEY | Cle AES-256-GCM (hex 64 chars) pour chiffrer les secrets OAuth en DB | - |
OAUTH_ENABLED | Active globalement OAuth | false |
OAUTH_REDIRECT_BASE_URL | URL de base pour les callbacks | http://localhost:3000 |
OAUTH_GOOGLE_CLIENT_ID | Google OAuth Client ID | - |
OAUTH_GOOGLE_CLIENT_SECRET | Google OAuth Client Secret | - |
OAUTH_GOOGLE_ENABLED | Active Google | false |
OAUTH_GITHUB_CLIENT_ID | GitHub OAuth Client ID | - |
OAUTH_GITHUB_CLIENT_SECRET | GitHub OAuth Client Secret | - |
OAUTH_GITHUB_ENABLED | Active GitHub | false |
OAUTH_DISCORD_CLIENT_ID | Discord OAuth Client ID | - |
OAUTH_DISCORD_CLIENT_SECRET | Discord OAuth Client Secret | - |
OAUTH_DISCORD_ENABLED | Active Discord | false |
OAUTH_MICROSOFT_CLIENT_ID | Microsoft OAuth Client ID | - |
OAUTH_MICROSOFT_CLIENT_SECRET | Microsoft OAuth Client Secret | - |
OAUTH_MICROSOFT_ENABLED | Active Microsoft | false |
OAUTH_MICROSOFT_TENANT | Azure AD tenant | common |
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_KEYdoit etre definie pour pouvoir enregistrer des secrets via l'interface admin. Generer avecopenssl 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
- L'utilisateur clique sur "Se connecter avec Google" sur
/login - Le handler
oauth_redirectgenere une URL d'autorisation avec unstateCSRF, unnonceOIDC et unpkce_verifier, les stocke en session, puis redirige vers le provider - L'utilisateur s'authentifie chez le provider
- Le provider redirige vers
/api/v1/auth/oauth/{provider}/callbackavec uncodeet lestate - Le handler
oauth_callback:- Valide le
stateen comparaison constant-time - Echange le
codecontre un token via PKCE - Extrait les informations utilisateur (email, nom, avatar)
- Appelle
OAuthService::find_or_create_userpour lier ou creer l'utilisateur - Genere un JWT + refresh token
- Regenere le session ID (anti-fixation)
- Redirige vers
/home
- Valide le
Routes OAuth
| Methode | Route | Description |
|---|---|---|
GET | /api/v1/auth/oauth/{provider}/redirect | Redirige vers le provider |
GET | /api/v1/auth/oauth/{provider}/callback | Traite le retour du provider |
GET | /api/v1/auth/oauth/providers | Liste les providers actifs (JSON) |
GET | /api/v1/auth/oauth/accounts | Liste les comptes OAuth de l'utilisateur |
DELETE | /api/v1/auth/oauth/accounts/{id} | Delie un compte OAuth |
Providers integres
| Provider | Protocole | Scopes | PKCE |
|---|---|---|---|
| OpenID Connect | openid email profile | S256 | |
| GitHub | OAuth2 + API REST | user:email | Non |
| Discord | OAuth2 + API REST | identify email | Non |
| Microsoft | OpenID Connect | openid email profile | S256 |
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
stategenere 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_userrefuse 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
| Hook | Signature | Declencheur |
|---|---|---|
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 :
- Cookie
locale(defini par les preferences utilisateur) - Header HTTP
Accept-Language - Variable d'environnement
DEFAULT_LOCALE
Ajouter une langue
- Creer
locales/pt.ymlavec les traductions - Ajouter
"pt"dans.supported_locales(vec!["fr", "en", "de", "es", "pt"]) - 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()).
| Tache | Module | Description |
|---|---|---|
| Email queue processor | tasks/email_queue_processor.rs | Envoie les emails en attente par batch |
| Password change email retry | tasks/password_change_email_retry.rs | Relance les confirmations de changement de mot de passe |
| Password change cleanup | tasks/password_change_cleanup.rs | Nettoie les demandes expirees |
| JWT blacklist cleanup | dans app.rs | Supprime les JWT expires de la blacklist |
| IMAP bounce processor | services/imap_bounce_service.rs | Verifie 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
| Middleware | Fichier | Description |
|---|---|---|
AuthService | middleware/auth_service.rs | Extraction JWT, injection Claims |
AdminService | middleware/admin_service.rs | Verification role admin |
auth_required | middleware/auth_required.rs | Protection route : 401 sans auth |
admin_required | middleware/admin_required.rs | Protection route : 403 sans admin |
csrf | middleware/csrf.rs | Protection CSRF (token dans session) |
rate_limit | middleware/rate_limit.rs | Limitation du debit par IP |
error_handler | middleware/error_handler.rs | Capture et formate les erreurs |
password_complexity | middleware/password_complexity.rs | Validation ANSSI des mots de passe |
update_last_login | middleware/update_last_login.rs | Met a jour last_login_at |
admin_extractor | middleware/admin_extractor.rs | Extracteur pour les pages admin |
admin_helper | middleware/admin_helper.rs | Fonctions 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_NAMEn'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 :
| Cle | Description |
|---|---|
app_name | Nom de l'application (peut etre modifie via /admin/settings) |
app_domain | Domaine de l'application |
app_description | Description pour les meta tags |
Les settings DB prennent priorite sur l'env pour l'affichage dans l'UI.
Commandes binaires
| Binaire | Fichier | Usage |
|---|---|---|
migrate | core/bin/ | Execute les migrations : cargo run --bin migrate |
seed | core/bin/seed.rs | Cree l'admin initial depuis les variables d'env |
hash_password | core/bin/hash_password.rs | Hashe un mot de passe bcrypt en ligne de commande |
change_password | core/bin/change_password.rs | Change 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(pasdebug) -
SESSION_BACKEND=seaormouredis(pasmemory) -
Si OAuth active :
OAUTH_ENCRYPTION_KEYest definie (64 caracteres hex) -
Si OAuth active :
OAUTH_REDIRECT_BASE_URLpointe 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.
GitRust