avant modification

EBO <eric.bouhana@softalys.com> committé le 2026-03-25 20:50

9e1c263cb2b64b1b29a2f88138802385986d558f

1 parent(s)

3 fichiers modifiés +394 -4
M crates/rustwarden-core/src/auth/oauth_handlers.rs
+5 -4
@@ -38,12 +38,12 @@ pub struct OAuthCallbackQuery {
38 38 /// Initiates the OAuth flow by redirecting the user to the provider's auth page.
39 39 /// Stores CSRF state, nonce, and PKCE verifier in the session.
40 40 pub async fn oauth_redirect(
41 - State(_db): State<DatabaseConnection>,
41 + State(db): State<DatabaseConnection>,
42 42 session: Session,
43 43 Extension(registry): Extension<Arc<ProviderRegistry>>,
44 44 Path(provider_name): Path<String>,
45 45 ) -> Result<Response, AppError> {
46 - let oauth_config = OAuthConfig::from_env();
46 + let oauth_config = OAuthConfig::load(&db).await;
47 47 if !oauth_config.enabled {
48 48 return Err(AppError::Validation("OAuth is not enabled".to_string()));
49 49 }
@@ -141,7 +141,7 @@ pub async fn oauth_callback(
141 141 session.remove::<String>(OAUTH_PKCE_KEY).await.ok();
142 142 session.remove::<String>(OAUTH_PROVIDER_KEY).await.ok();
143 143
144 - let oauth_config = OAuthConfig::from_env();
144 + let oauth_config = OAuthConfig::load(&db).await;
145 145 let redirect_uri = oauth_config.callback_url(&provider_name);
146 146
147 147 let provider = registry
@@ -255,9 +255,10 @@ pub async fn oauth_callback(
255 255 ///
256 256 /// Returns the list of enabled OAuth providers (for the login page to display buttons).
257 257 pub async fn list_providers(
258 + State(db): State<DatabaseConnection>,
258 259 Extension(registry): Extension<Arc<ProviderRegistry>>,
259 260 ) -> Json<Vec<serde_json::Value>> {
260 - let oauth_config = OAuthConfig::from_env();
261 + let oauth_config = OAuthConfig::load(&db).await;
261 262 if !oauth_config.enabled {
262 263 return Json(vec![]);
263 264 }
A crates/rustwarden-ui/templates/admin/dashboard.html
+195 -0
@@ -0,0 +1,195 @@
1 +{% extends "base.html" %}
2 +
3 +{% block title %}{{ page_title }}{% endblock %}
4 +
5 +{% block navbar %}
6 +{% include "components/navbar.html" %}
7 +{% endblock %}
8 +
9 +{% block content %}
10 +<div class="container mx-auto px-4 py-4">
11 + <!-- Breadcrumb -->
12 + <div class="text-sm breadcrumbs mb-3">
13 + <ul>
14 + <li><a href="/">{{ breadcrumb_home }}</a></li>
15 + <li>{{ breadcrumb_admin }}</li>
16 + </ul>
17 + </div>
18 +
19 + <h1 class="text-2xl font-bold mb-4">{{ heading }}</h1>
20 +
21 + <!-- Statistics Cards -->
22 + <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
23 + <!-- Total Users Card -->
24 + <div class="card bg-base-100 shadow-md">
25 + <div class="card-body p-4">
26 + <h2 class="card-title text-primary text-base">👥 {{ users_card_title }}</h2>
27 + <p class="text-xs text-base-content/70 mb-1">{{ users_card_subtitle }}</p>
28 + <p class="text-2xl font-bold">{{ total_users }}</p>
29 + <div class="card-actions justify-end mt-2">
30 + <a href="/admin/users" class="btn btn-primary btn-sm">{{ access_button }}</a>
31 + </div>
32 + </div>
33 + </div>
34 +
35 + <!-- Active Sessions Card -->
36 + <div class="card bg-base-100 shadow-md">
37 + <div class="card-body p-4">
38 + <h2 class="card-title text-secondary text-base">🔐 {{ sessions_card_title }}</h2>
39 + <p class="text-xs text-base-content/70 mb-1">{{ sessions_card_subtitle }}</p>
40 + <p class="text-2xl font-bold">{{ active_sessions }}</p>
41 + </div>
42 + </div>
43 +
44 + <!-- Admins Card -->
45 + <div class="card bg-base-100 shadow-md">
46 + <div class="card-body p-4">
47 + <h2 class="card-title text-accent text-base">👑 {{ admins_card_title }}</h2>
48 + <p class="text-xs text-base-content/70 mb-1">{{ admins_card_subtitle }}</p>
49 + <p class="text-2xl font-bold">{{ total_admins }}</p>
50 + </div>
51 + </div>
52 +
53 + <!-- Last Activity Card -->
54 + <div class="card bg-base-100 shadow-md">
55 + <div class="card-body p-4">
56 + <h2 class="card-title text-info text-base">⏰ {{ last_activity_card_title }}</h2>
57 + <p class="text-xs text-base-content/70 mb-1">{{ last_activity_card_subtitle }}</p>
58 + <p class="text-base font-semibold">{{ last_activity }}</p>
59 + </div>
60 + </div>
61 + </div>
62 +
63 + <!-- Audit Log Statistics (if available) -->
64 + {% if has_audit_logs %}
65 + <div class="card bg-base-100 shadow-md mb-4">
66 + <div class="card-body p-4">
67 + <h2 class="card-title text-lg mb-3">📊 {{ audit_section_title }}</h2>
68 + <div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
69 + <!-- Histogram Chart -->
70 + <div>
71 + <canvas id="audit-logs-chart" width="400" height="200"></canvas>
72 + </div>
73 + <!-- Statistics and Legends -->
74 + <div class="space-y-3">
75 + <div class="grid grid-cols-2 gap-3">
76 + <div>
77 + <p class="text-xs text-base-content/70">{{ audit_total_logs_label }}</p>
78 + <p class="text-xl font-bold">{{ audit_total_logs }}</p>
79 + </div>
80 + <div>
81 + <p class="text-xs text-base-content/70">{{ audit_last_24h }}</p>
82 + <p class="text-xl font-bold">{{ audit_logs_last_24h }}</p>
83 + </div>
84 + </div>
85 + <div>
86 + <p class="text-xs text-base-content/70 mb-2">{{ audit_by_level }}</p>
87 + <div class="flex flex-wrap gap-2">
88 + <span class="badge badge-info badge-sm">DEBUG: {{ audit_debug_count }}</span>
89 + <span class="badge badge-success badge-sm">INFO: {{ audit_info_count }}</span>
90 + <span class="badge badge-warning badge-sm">WARN: {{ audit_warn_count }}</span>
91 + <span class="badge badge-error badge-sm">ERROR: {{ audit_error_count }}</span>
92 + </div>
93 + </div>
94 + </div>
95 + </div>
96 + </div>
97 + </div>
98 + {% endif %}
99 +
100 + <!-- Navigation Links -->
101 + <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
102 + <a href="/admin/roles" class="card bg-base-100 shadow-md hover:shadow-lg transition-shadow">
103 + <div class="card-body p-4">
104 + <h2 class="card-title text-warning text-base">🛡️ {{ roles_card_title }}</h2>
105 + <p class="text-sm">{{ roles_card_desc }}</p>
106 + <div class="stats shadow-sm mt-2">
107 + <div class="stat py-1 px-2">
108 + <div class="stat-title text-xs">{{ roles_stat_title }}</div>
109 + <div class="stat-value text-base">{{ total_roles }}</div>
110 + </div>
111 + </div>
112 + <div class="card-actions justify-end mt-2">
113 + <button class="btn btn-warning btn-sm">{{ access_button }}</button>
114 + </div>
115 + </div>
116 + </a>
117 +
118 + <a href="/admin/settings" class="card bg-base-100 shadow-md hover:shadow-lg transition-shadow">
119 + <div class="card-body p-4">
120 + <h2 class="card-title text-secondary text-base">⚙️ {{ settings_card_title }}</h2>
121 + <p class="text-sm">{{ settings_card_desc }}</p>
122 + <div class="card-actions justify-end mt-2">
123 + <button class="btn btn-secondary btn-sm">{{ access_button }}</button>
124 + </div>
125 + </div>
126 + </a>
127 +
128 + <a href="/admin/apps" class="card bg-base-100 shadow-md hover:shadow-lg transition-shadow">
129 + <div class="card-body p-4">
130 + <h2 class="card-title text-accent text-base">📦 {{ apps_card_title }}</h2>
131 + <p class="text-sm">{{ apps_card_desc }}</p>
132 + <div class="card-actions justify-end mt-2">
133 + <button class="btn btn-accent btn-sm">{{ access_button }}</button>
134 + </div>
135 + </div>
136 + </a>
137 +
138 + </div>
139 +</div>
140 +{% endblock %}
141 +
142 +{% block scripts %}
143 +{% if has_audit_logs %}
144 +<script src="/static/js/chart.umd.min.js"></script>
145 +<script>
146 +document.addEventListener('DOMContentLoaded', function() {
147 + const ctx = document.getElementById('audit-logs-chart');
148 + if (ctx) {
149 + new Chart(ctx, {
150 + type: 'bar',
151 + data: {
152 + labels: ['DEBUG', 'INFO', 'WARN', 'ERROR'],
153 + datasets: [{
154 + label: '{{ chart_label }}',
155 + data: [
156 + {{ audit_debug_count }},
157 + {{ audit_info_count }},
158 + {{ audit_warn_count }},
159 + {{ audit_error_count }}
160 + ],
161 + backgroundColor: [
162 + 'rgba(59, 130, 246, 0.5)', // info
163 + 'rgba(34, 197, 94, 0.5)', // success
164 + 'rgba(251, 191, 36, 0.5)', // warning
165 + 'rgba(239, 68, 68, 0.5)' // error
166 + ],
167 + borderColor: [
168 + 'rgba(59, 130, 246, 1)',
169 + 'rgba(34, 197, 94, 1)',
170 + 'rgba(251, 191, 36, 1)',
171 + 'rgba(239, 68, 68, 1)'
172 + ],
173 + borderWidth: 1
174 + }]
175 + },
176 + options: {
177 + responsive: true,
178 + maintainAspectRatio: true,
179 + scales: {
180 + y: {
181 + beginAtZero: true
182 + }
183 + },
184 + plugins: {
185 + legend: {
186 + display: false
187 + }
188 + }
189 + }
190 + });
191 + }
192 +});
193 +</script>
194 +{% endif %}
195 +{% endblock %}
A docs/project_rustwarden_db_getter.md
+194 -0
@@ -0,0 +1,194 @@
1 +---
2 +name: Rustwarden DB getter improvement
3 +description: RustwardenApp manque d'un getter database() — workaround actuel dans main.rs, amelioration upstream a envisager
4 +type: project
5 +---
6 +
7 +# Specification : Exposition de getters sur `RustwardenApp`
8 +
9 +**Version** : 1.0
10 +**Date** : 2026-03-25
11 +**Composant** : `rustwarden-core` (crate framework)
12 +**Statut** : Proposition
13 +
14 +---
15 +
16 +## 1. Contexte et probleme
17 +
18 +`RustwardenApp` est une structure opaque qui ne contient que deux champs prives :
19 +
20 +```rust
21 +pub struct RustwardenApp {
22 + router: Router,
23 + address: String,
24 +}
25 +```
26 +
27 +Apres construction via `builder().build().await?`, le consommateur n'a **aucun acces** aux ressources internes (DB, config, hooks, i18n...). La seule action possible est `run()` (demarrer le serveur) ou `into_router()` (recuperer le router pour les tests).
28 +
29 +### Le workaround actuel dans les projets consommateurs
30 +
31 +```rust
32 +// main.rs d'un projet consommateur (ex: PasterMan, GitRust)
33 +let db = rustwarden_core::config::database::create_connection().await?;
34 +my_app::migrations::run_migrations(&db).await?;
35 +
36 +let app = RustwardenApp::builder()
37 + .from_env()
38 + .database(db.clone()) // <-- clone obligatoire
39 + .auto_migrate(false)
40 + .merge_routes(my_app_routes)
41 + .build().await?;
42 +
43 +// Utilisation de `db` (le clone) pour les operations metier
44 +// L'app et le main partagent deux copies independantes
45 +app.run().await
46 +```
47 +
48 +**Problemes concrets** :
49 +- Le `db.clone()` cree une **copie du pool de connexions** — les deux copies (builder et main) partagent le meme pool sous-jacent, mais la duplication est semantiquement trompeuse
50 +- Le consommateur doit gerer le cycle de vie de la DB **en dehors** du framework, ce qui va a l'encontre du builder pattern
51 +- Si le consommateur oublie `.database(db.clone())` et laisse le builder creer sa propre connexion, il se retrouve avec **deux pools distincts** vers la meme base — gaspillage de ressources et risque de coherence
52 +- Aucun acces a la config auth, a l'i18n, ou aux hooks apres construction — impossible de les reutiliser dans du code metier
53 +
54 +## 2. Objectif
55 +
56 +Ajouter des getters sur `RustwardenApp` pour exposer les ressources internes aux projets consommateurs, tout en preservant l'encapsulation du framework.
57 +
58 +## 3. Changements proposes
59 +
60 +### 3.1 Stocker la DB dans `RustwardenApp`
61 +
62 +**Fichier** : `crates/rustwarden-core/src/app.rs`
63 +
64 +```rust
65 +pub struct RustwardenApp {
66 + router: Router,
67 + address: String,
68 + db: DatabaseConnection, // Nouveau
69 + auth_config: Arc<AuthConfig>, // Nouveau
70 +}
71 +```
72 +
73 +Dans `build()`, avant de consommer `db` dans les layers, stocker une reference :
74 +
75 +```rust
76 +let db_for_app = db.clone(); // un seul clone, pour le struct
77 +// ... (construction du router avec `db`) ...
78 +
79 +Ok(RustwardenApp {
80 + router: app,
81 + address,
82 + db: db_for_app,
83 + auth_config,
84 +})
85 +```
86 +
87 +### 3.2 Getters
88 +
89 +```rust
90 +impl RustwardenApp {
91 + /// Acces a la connexion base de donnees du framework.
92 + ///
93 + /// Permet aux projets consommateurs d'executer des requetes metier
94 + /// sur la meme connexion (pool) que le framework.
95 + pub fn database(&self) -> &DatabaseConnection {
96 + &self.db
97 + }
98 +
99 + /// Acces a la configuration d'authentification.
100 + pub fn auth_config(&self) -> &AuthConfig {
101 + &self.auth_config
102 + }
103 +
104 + /// Adresse de bind du serveur (ex: "0.0.0.0:3000").
105 + pub fn address(&self) -> &str {
106 + &self.address
107 + }
108 +
109 + // Les getters existants restent inchanges :
110 + // pub fn into_router(self) -> Router
111 + // pub async fn run(self) -> anyhow::Result<()>
112 +}
113 +```
114 +
115 +### 3.3 Usage consommateur apres modification
116 +
117 +```rust
118 +// main.rs — APRES amelioration
119 +let app = RustwardenApp::builder()
120 + .from_env()
121 + .auto_migrate(true)
122 + .merge_routes(my_app_routes)
123 + .build().await?;
124 +
125 +// Le consommateur accede a la DB directement depuis l'app
126 +my_app::migrations::run_migrations(app.database()).await?;
127 +my_app::seed::seed_data(app.database()).await?;
128 +
129 +app.run().await
130 +```
131 +
132 +Plus besoin de `create_connection()` ni de `.clone()` dans le main.
133 +
134 +## 4. Analyse des risques
135 +
136 +### 4.1 Risques du changement
137 +
138 +| Risque | Severite | Mitigation |
139 +|--------|----------|------------|
140 +| **Breaking change sur `RustwardenApp`** (ajout de champs) | **Nulle** | La struct n'est pas constructible de l'exterieur (pas de `pub` sur les champs, pas de `#[non_exhaustive]`). Seul `builder().build()` la cree. L'ajout de champs prives est retrocompatible. |
141 +| **Exposition excessive de la DB** | Moyenne | Le getter retourne `&DatabaseConnection` (reference immutable). Le consommateur peut faire des requetes mais ne peut pas fermer/reconfigurer la connexion. C'est le meme niveau d'acces que `State(db)` dans les handlers Axum. |
142 +| **Clone supplementaire du pool** | Faible | `DatabaseConnection` dans SeaORM est un `Arc<Pool>` — le clone est une copie de pointeur (O(1), quelques octets). Cout negligeable. |
143 +| **Utilisation apres `run()`** | Nulle | `run()` consomme `self` (`self`, pas `&self`). Apres `run()`, l'app n'existe plus — aucun risque d'acces concurrent via le getter. |
144 +| **Acces DB entre `build()` et `run()`** | Faible | Fenetre d'acces intentionnelle et utile (migrations metier, seeding). Les taches de fond (email queue, JWT cleanup) tournent deja dans cette fenetre via des `tokio::spawn` — pas de nouveau risque. |
145 +
146 +### 4.2 Risques de NE PAS faire le changement
147 +
148 +| Risque | Severite | Impact |
149 +|--------|----------|--------|
150 +| **Duplication du pattern DB dans chaque consommateur** | Moyenne | Chaque nouveau projet doit rediger le meme boilerplate `create_connection()` + `clone()`. Erreur frequente pour les nouveaux consommateurs. |
151 +| **Deux pools de connexions accidentels** | Elevee | Si un consommateur oublie `.database(db.clone())`, le builder cree un second pool. Doublement des connexions PostgreSQL, risque de saturation en production. |
152 +| **Impossibilite de migrer apres build** | Moyenne | Le pattern actuel force a migrer AVANT `build()`. Les consommateurs qui veulent `auto_migrate(true)` pour les tables framework mais ensuite migrer leurs propres tables ne peuvent pas le faire proprement. |
153 +| **Framework non idiomatique** | Faible | Le builder pattern en Rust expose generalement des getters sur le produit construit. L'absence degrade l'ergonomie et la credibilite du framework. |
154 +
155 +## 5. Avantages
156 +
157 +| Avantage | Impact |
158 +|----------|--------|
159 +| **Elimination du boilerplate** | Chaque consommateur economise ~5 lignes de gestion DB dans main.rs |
160 +| **Un seul pool garanti** | Impossible d'avoir accidentellement deux pools vers la meme DB |
161 +| **Cycle de vie centralise** | Le framework possede la DB de A a Z : creation, migration, exposition, fermeture |
162 +| **Migrations metier post-build** | Le consommateur peut `app.database()` pour migrer ses propres tables apres que le framework ait migre les siennes via `auto_migrate(true)` |
163 +| **Testabilite** | Les tests d'integration peuvent `app.database()` pour setup/teardown les donnees sans reconstruire une connexion |
164 +| **Extensibilite future** | Ouvre la voie a d'autres getters (`i18n_service()`, `hooks()`) sans repenser l'architecture |
165 +
166 +## 6. Retrocompatibilite
167 +
168 +| Composant | Impact |
169 +|-----------|--------|
170 +| `RustwardenApp` struct | Aucun — ajout de champs prives uniquement |
171 +| `builder().build()` | Aucun — la signature de retour ne change pas |
172 +| `run()` | Aucun — consomme toujours `self` |
173 +| `into_router()` | Aucun — consomme toujours `self` |
174 +| Projets consommateurs existants | Aucun — le pattern `db.clone()` continue de fonctionner. Les getters sont additifs. |
175 +| Prelude | Aucun changement requis — `RustwardenApp` est deja exporte |
176 +| CLI wizard templates | Aucun changement requis — le code genere peut adopter le nouveau pattern progressivement |
177 +
178 +## 7. Fichiers impactes
179 +
180 +| Fichier | Modification |
181 +|---------|-------------|
182 +| `crates/rustwarden-core/src/app.rs` | Ajout de `db` et `auth_config` dans `RustwardenApp`, getters, clone dans `build()` |
183 +
184 +Un seul fichier impacte. Aucune migration, aucun changement de dependance.
185 +
186 +## 8. Criteres d'acceptation
187 +
188 +- [ ] `app.database()` retourne `&DatabaseConnection` fonctionnelle apres `build()`
189 +- [ ] `app.auth_config()` retourne `&AuthConfig` fonctionnelle apres `build()`
190 +- [ ] `app.address()` retourne l'adresse de bind
191 +- [ ] Les projets consommateurs existants compilent sans modification
192 +- [ ] Le pattern `builder().database(db).build()` continue de fonctionner (retrocompat)
193 +- [ ] `cargo clippy -- -D warnings` passe sans avertissement
194 +- [ ] `cargo test` — les tests existants passent sans modification