feat: routes modules (auth, dashboard, settings, alerts, users)
This commit is contained in:
@@ -1 +1,53 @@
|
|||||||
// Alerts routes — Task 10
|
use axum::{
|
||||||
|
extract::State,
|
||||||
|
response::{IntoResponse, Redirect},
|
||||||
|
};
|
||||||
|
use tower_sessions::Session;
|
||||||
|
|
||||||
|
use crate::routes::{flash, get_and_clear_flash, render_html, AppState, AuthUser};
|
||||||
|
|
||||||
|
pub async fn alerts_get(
|
||||||
|
_auth: AuthUser,
|
||||||
|
session: Session,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let flash_messages = get_and_clear_flash(&session).await;
|
||||||
|
let raw_alerts = {
|
||||||
|
let cm = state.config_manager.lock().await;
|
||||||
|
cm.load_alerts()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Formater les timestamps (ISO → "YYYY-MM-DD HH:MM:SS")
|
||||||
|
let alerts: Vec<serde_json::Value> = raw_alerts
|
||||||
|
.iter()
|
||||||
|
.map(|a| {
|
||||||
|
let mut v = serde_json::to_value(a).unwrap();
|
||||||
|
if let Some(ts) = v.get("timestamp").and_then(|t| t.as_str()) {
|
||||||
|
let formatted = ts.chars().take(19).collect::<String>().replace('T', " ");
|
||||||
|
v["timestamp_display"] = serde_json::Value::String(formatted);
|
||||||
|
}
|
||||||
|
v
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut ctx = tera::Context::new();
|
||||||
|
ctx.insert("flash_messages", &flash_messages);
|
||||||
|
ctx.insert("is_authenticated", &true);
|
||||||
|
ctx.insert("active_page", "alerts");
|
||||||
|
ctx.insert("alerts", &alerts);
|
||||||
|
|
||||||
|
render_html(&state.tera, "alerts.html", ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn clear_alerts(
|
||||||
|
_auth: AuthUser,
|
||||||
|
session: Session,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
{
|
||||||
|
let cm = state.config_manager.lock().await;
|
||||||
|
cm.clear_alerts();
|
||||||
|
}
|
||||||
|
flash(&session, "success", "Historique des alertes effacé.").await;
|
||||||
|
Redirect::to("/alerts")
|
||||||
|
}
|
||||||
|
|||||||
@@ -1 +1,67 @@
|
|||||||
// Auth routes — Task 7
|
use axum::{
|
||||||
|
extract::{Form, State},
|
||||||
|
response::{IntoResponse, Redirect},
|
||||||
|
};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use tower_sessions::Session;
|
||||||
|
|
||||||
|
use crate::routes::{flash, get_and_clear_flash, render_html, AppState, SESSION_USER_KEY};
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct LoginForm {
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn login_get(
|
||||||
|
session: Session,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
// Si déjà connecté, rediriger vers dashboard
|
||||||
|
if session
|
||||||
|
.get::<String>(SESSION_USER_KEY)
|
||||||
|
.await
|
||||||
|
.unwrap_or_default()
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
return Redirect::to("/").into_response();
|
||||||
|
}
|
||||||
|
let flash_messages = get_and_clear_flash(&session).await;
|
||||||
|
let mut ctx = tera::Context::new();
|
||||||
|
ctx.insert("flash_messages", &flash_messages);
|
||||||
|
ctx.insert("is_authenticated", &false);
|
||||||
|
render_html(&state.tera, "login.html", ctx).into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn login_post(
|
||||||
|
session: Session,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Form(form): Form<LoginForm>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let (admin_username, admin_hash) = {
|
||||||
|
let cm = state.config_manager.lock().await;
|
||||||
|
(
|
||||||
|
cm.config.admin.username.clone(),
|
||||||
|
cm.config.admin.password_hash.clone(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let username = form.username.trim().to_string();
|
||||||
|
let password = form.password.clone();
|
||||||
|
|
||||||
|
let valid = username == admin_username
|
||||||
|
&& bcrypt::verify(&password, &admin_hash).unwrap_or(false);
|
||||||
|
|
||||||
|
if valid {
|
||||||
|
session.insert(SESSION_USER_KEY, username).await.ok();
|
||||||
|
return Redirect::to("/").into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
flash(&session, "danger", "Identifiants incorrects.").await;
|
||||||
|
Redirect::to("/login").into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn logout(session: Session) -> impl IntoResponse {
|
||||||
|
session.flush().await.ok();
|
||||||
|
Redirect::to("/login")
|
||||||
|
}
|
||||||
|
|||||||
@@ -1 +1,63 @@
|
|||||||
// Dashboard routes — Task 8
|
use axum::{
|
||||||
|
extract::State,
|
||||||
|
response::{IntoResponse, Json, Redirect},
|
||||||
|
};
|
||||||
|
use tower_sessions::Session;
|
||||||
|
|
||||||
|
use crate::routes::{flash, get_and_clear_flash, render_html, AppState, AuthUser};
|
||||||
|
|
||||||
|
pub async fn dashboard(
|
||||||
|
_auth: AuthUser,
|
||||||
|
session: Session,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let flash_messages = get_and_clear_flash(&session).await;
|
||||||
|
let metrics = state.monitor.metrics.read().unwrap().clone();
|
||||||
|
|
||||||
|
let (is_default_pw, _admin_username) = {
|
||||||
|
let cm = state.config_manager.lock().await;
|
||||||
|
let hash = cm.config.admin.password_hash.clone();
|
||||||
|
let username = cm.config.admin.username.clone();
|
||||||
|
(
|
||||||
|
bcrypt::verify("admin", &hash).unwrap_or(false),
|
||||||
|
username,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut ctx = tera::Context::new();
|
||||||
|
ctx.insert("flash_messages", &flash_messages);
|
||||||
|
ctx.insert("is_authenticated", &true);
|
||||||
|
ctx.insert("active_page", "dashboard");
|
||||||
|
ctx.insert("metrics", &metrics);
|
||||||
|
ctx.insert("default_pw", &is_default_pw);
|
||||||
|
|
||||||
|
render_html(&state.tera, "dashboard.html", ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn api_metrics(
|
||||||
|
_auth: AuthUser,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let metrics = state.monitor.metrics.read().unwrap().clone();
|
||||||
|
Json(metrics)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn toggle_monitoring(
|
||||||
|
_auth: AuthUser,
|
||||||
|
session: Session,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let is_running = state
|
||||||
|
.monitor
|
||||||
|
.running
|
||||||
|
.load(std::sync::atomic::Ordering::Relaxed);
|
||||||
|
if is_running {
|
||||||
|
state.monitor.stop();
|
||||||
|
flash(&session, "warning", "Monitoring arrêté.").await;
|
||||||
|
} else {
|
||||||
|
let m = state.monitor.clone();
|
||||||
|
m.start().await;
|
||||||
|
flash(&session, "success", "Monitoring démarré.").await;
|
||||||
|
}
|
||||||
|
Redirect::to("/")
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,3 +3,127 @@ pub mod dashboard;
|
|||||||
pub mod settings;
|
pub mod settings;
|
||||||
pub mod alerts;
|
pub mod alerts;
|
||||||
pub mod users;
|
pub mod users;
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
async_trait,
|
||||||
|
extract::FromRequestParts,
|
||||||
|
http::request::Parts,
|
||||||
|
response::{IntoResponse, Redirect, Response},
|
||||||
|
};
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
use tera::Tera;
|
||||||
|
use tokio::sync::Mutex as AsyncMutex;
|
||||||
|
use tower_sessions::Session;
|
||||||
|
|
||||||
|
use crate::alerter::Alerter;
|
||||||
|
use crate::config::ConfigManager;
|
||||||
|
use crate::monitor::SystemMonitor;
|
||||||
|
use crate::user_monitor::UserMonitor;
|
||||||
|
|
||||||
|
pub const SESSION_USER_KEY: &str = "username";
|
||||||
|
pub const SESSION_FLASH_KEY: &str = "flash_messages";
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AppState {
|
||||||
|
pub config_manager: Arc<AsyncMutex<ConfigManager>>,
|
||||||
|
pub monitor: Arc<SystemMonitor>,
|
||||||
|
pub alerter: Arc<Alerter>,
|
||||||
|
pub user_monitor: Arc<UserMonitor>,
|
||||||
|
pub tera: Arc<Tera>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppState {
|
||||||
|
pub fn new(
|
||||||
|
config_manager: Arc<AsyncMutex<ConfigManager>>,
|
||||||
|
monitor: Arc<SystemMonitor>,
|
||||||
|
alerter: Arc<Alerter>,
|
||||||
|
user_monitor: Arc<UserMonitor>,
|
||||||
|
) -> Self {
|
||||||
|
let tera = build_tera();
|
||||||
|
AppState {
|
||||||
|
config_manager,
|
||||||
|
monitor,
|
||||||
|
alerter,
|
||||||
|
user_monitor,
|
||||||
|
tera: Arc::new(tera),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_tera() -> Tera {
|
||||||
|
let mut tera = Tera::default();
|
||||||
|
tera.add_raw_templates(vec![
|
||||||
|
("base.html", include_str!("../../templates/base.html")),
|
||||||
|
("login.html", include_str!("../../templates/login.html")),
|
||||||
|
(
|
||||||
|
"dashboard.html",
|
||||||
|
include_str!("../../templates/dashboard.html"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"settings.html",
|
||||||
|
include_str!("../../templates/settings.html"),
|
||||||
|
),
|
||||||
|
("alerts.html", include_str!("../../templates/alerts.html")),
|
||||||
|
("users.html", include_str!("../../templates/users.html")),
|
||||||
|
])
|
||||||
|
.expect("Erreur chargement templates Tera");
|
||||||
|
tera
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn flash(session: &Session, category: &str, message: &str) {
|
||||||
|
let mut messages: Vec<(String, String)> = session
|
||||||
|
.get::<Vec<(String, String)>>(SESSION_FLASH_KEY)
|
||||||
|
.await
|
||||||
|
.unwrap_or_default()
|
||||||
|
.unwrap_or_default();
|
||||||
|
messages.push((category.to_string(), message.to_string()));
|
||||||
|
session.insert(SESSION_FLASH_KEY, messages).await.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_and_clear_flash(session: &Session) -> Vec<(String, String)> {
|
||||||
|
let messages: Vec<(String, String)> = session
|
||||||
|
.get::<Vec<(String, String)>>(SESSION_FLASH_KEY)
|
||||||
|
.await
|
||||||
|
.unwrap_or_default()
|
||||||
|
.unwrap_or_default();
|
||||||
|
session
|
||||||
|
.remove::<Vec<(String, String)>>(SESSION_FLASH_KEY)
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
messages
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AuthUser(pub String);
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<S> FromRequestParts<S> for AuthUser
|
||||||
|
where
|
||||||
|
S: Send + Sync,
|
||||||
|
{
|
||||||
|
type Rejection = Response;
|
||||||
|
|
||||||
|
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||||
|
let session = Session::from_request_parts(parts, state)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.into_response())?;
|
||||||
|
match session
|
||||||
|
.get::<String>(SESSION_USER_KEY)
|
||||||
|
.await
|
||||||
|
.unwrap_or_default()
|
||||||
|
{
|
||||||
|
Some(u) => Ok(AuthUser(u)),
|
||||||
|
None => Err(Redirect::to("/login").into_response()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_html(
|
||||||
|
tera: &Tera,
|
||||||
|
template: &str,
|
||||||
|
ctx: tera::Context,
|
||||||
|
) -> axum::response::Html<String> {
|
||||||
|
match tera.render(template, &ctx) {
|
||||||
|
Ok(html) => axum::response::Html(html),
|
||||||
|
Err(e) => axum::response::Html(format!("<pre>Erreur template: {}</pre>", e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1 +1,398 @@
|
|||||||
// Settings routes — Task 9
|
use axum::{
|
||||||
|
extract::{Form, State},
|
||||||
|
response::{IntoResponse, Redirect},
|
||||||
|
};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use tower_sessions::Session;
|
||||||
|
|
||||||
|
use crate::routes::{flash, get_and_clear_flash, render_html, AppState, AuthUser};
|
||||||
|
|
||||||
|
pub async fn settings_get(
|
||||||
|
_auth: AuthUser,
|
||||||
|
session: Session,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let flash_messages = get_and_clear_flash(&session).await;
|
||||||
|
let (config, is_default_pw) = {
|
||||||
|
let cm = state.config_manager.lock().await;
|
||||||
|
let pw_default =
|
||||||
|
bcrypt::verify("admin", &cm.config.admin.password_hash).unwrap_or(false);
|
||||||
|
(cm.config.clone(), pw_default)
|
||||||
|
};
|
||||||
|
|
||||||
|
let smtp_password_masked = if config.smtp.password.is_empty() {
|
||||||
|
"".to_string()
|
||||||
|
} else {
|
||||||
|
"********".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut ctx = tera::Context::new();
|
||||||
|
ctx.insert("flash_messages", &flash_messages);
|
||||||
|
ctx.insert("is_authenticated", &true);
|
||||||
|
ctx.insert("active_page", "settings");
|
||||||
|
ctx.insert("config", &config);
|
||||||
|
ctx.insert("smtp", &config.smtp);
|
||||||
|
ctx.insert("smtp_password_masked", &smtp_password_masked);
|
||||||
|
ctx.insert("default_pw", &is_default_pw);
|
||||||
|
|
||||||
|
render_html(&state.tera, "settings.html", ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct ThresholdsForm {
|
||||||
|
pub cpu_percent: u32,
|
||||||
|
pub ram_percent: u32,
|
||||||
|
pub disk_percent: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_thresholds(
|
||||||
|
_auth: AuthUser,
|
||||||
|
session: Session,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Form(form): Form<ThresholdsForm>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
if !(1..=100).contains(&form.cpu_percent)
|
||||||
|
|| !(1..=100).contains(&form.ram_percent)
|
||||||
|
|| !(1..=100).contains(&form.disk_percent)
|
||||||
|
{
|
||||||
|
flash(
|
||||||
|
&session,
|
||||||
|
"danger",
|
||||||
|
"Les seuils doivent être entre 1 et 100.",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return Redirect::to("/settings");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mut cm = state.config_manager.lock().await;
|
||||||
|
cm.config.thresholds.cpu_percent = form.cpu_percent as f64;
|
||||||
|
cm.config.thresholds.ram_percent = form.ram_percent as f64;
|
||||||
|
cm.config.thresholds.disk_percent = form.disk_percent as f64;
|
||||||
|
cm.save();
|
||||||
|
}
|
||||||
|
flash(&session, "success", "Seuils mis à jour.").await;
|
||||||
|
Redirect::to("/settings")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct MonitoringForm {
|
||||||
|
pub check_interval_minutes: u64,
|
||||||
|
pub alert_cooldown_minutes: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_monitoring(
|
||||||
|
_auth: AuthUser,
|
||||||
|
session: Session,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Form(form): Form<MonitoringForm>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
if form.check_interval_minutes < 1 {
|
||||||
|
flash(
|
||||||
|
&session,
|
||||||
|
"danger",
|
||||||
|
"L'intervalle doit être d'au moins 1 minute.",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return Redirect::to("/settings");
|
||||||
|
}
|
||||||
|
if form.alert_cooldown_minutes < 1 {
|
||||||
|
flash(
|
||||||
|
&session,
|
||||||
|
"danger",
|
||||||
|
"Le cooldown doit être d'au moins 1 minute.",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return Redirect::to("/settings");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mut cm = state.config_manager.lock().await;
|
||||||
|
cm.config.check_interval_minutes = form.check_interval_minutes;
|
||||||
|
cm.config.alert_cooldown_minutes = form.alert_cooldown_minutes;
|
||||||
|
cm.save();
|
||||||
|
}
|
||||||
|
flash(
|
||||||
|
&session,
|
||||||
|
"success",
|
||||||
|
"Paramètres de monitoring mis à jour.",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
Redirect::to("/settings")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct SmtpForm {
|
||||||
|
pub smtp_server: String,
|
||||||
|
pub smtp_port: u16,
|
||||||
|
pub smtp_tls: Option<String>,
|
||||||
|
pub smtp_username: String,
|
||||||
|
pub smtp_password: Option<String>,
|
||||||
|
pub smtp_from: String,
|
||||||
|
pub smtp_to: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_smtp(
|
||||||
|
_auth: AuthUser,
|
||||||
|
session: Session,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Form(form): Form<SmtpForm>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let to_emails: Vec<String> = form
|
||||||
|
.smtp_to
|
||||||
|
.split(',')
|
||||||
|
.map(|s| s.trim().to_string())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.collect();
|
||||||
|
{
|
||||||
|
let mut cm = state.config_manager.lock().await;
|
||||||
|
let old_password = cm.config.smtp.password.clone();
|
||||||
|
cm.config.smtp.server = form.smtp_server.trim().to_string();
|
||||||
|
cm.config.smtp.port = form.smtp_port;
|
||||||
|
cm.config.smtp.use_tls = form.smtp_tls.is_some();
|
||||||
|
cm.config.smtp.username = form.smtp_username.trim().to_string();
|
||||||
|
cm.config.smtp.from_email = form.smtp_from.trim().to_string();
|
||||||
|
cm.config.smtp.to_emails = to_emails;
|
||||||
|
cm.config.smtp.password = match form.smtp_password {
|
||||||
|
Some(pw) if !pw.is_empty() => pw,
|
||||||
|
_ => old_password,
|
||||||
|
};
|
||||||
|
cm.save();
|
||||||
|
}
|
||||||
|
flash(&session, "success", "Configuration SMTP mise à jour.").await;
|
||||||
|
Redirect::to("/settings")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn test_smtp(
|
||||||
|
_auth: AuthUser,
|
||||||
|
session: Session,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let smtp = {
|
||||||
|
let cm = state.config_manager.lock().await;
|
||||||
|
cm.config.smtp.clone()
|
||||||
|
};
|
||||||
|
let (ok, msg) = state.alerter.send_test(&smtp).await;
|
||||||
|
if ok {
|
||||||
|
flash(&session, "success", &format!("Test réussi : {}", msg)).await;
|
||||||
|
} else {
|
||||||
|
flash(&session, "danger", &format!("Test échoué : {}", msg)).await;
|
||||||
|
}
|
||||||
|
Redirect::to("/settings")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct ProcessesForm {
|
||||||
|
#[serde(rename = "proc_name[]")]
|
||||||
|
pub proc_name: Option<Vec<String>>,
|
||||||
|
#[serde(rename = "proc_pattern[]")]
|
||||||
|
pub proc_pattern: Option<Vec<String>>,
|
||||||
|
#[serde(rename = "proc_mem_threshold[]")]
|
||||||
|
pub proc_mem_threshold: Option<Vec<String>>,
|
||||||
|
#[serde(rename = "proc_enabled[]")]
|
||||||
|
pub proc_enabled: Option<Vec<String>>,
|
||||||
|
#[serde(rename = "proc_alert_down[]")]
|
||||||
|
pub proc_alert_down: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_processes(
|
||||||
|
_auth: AuthUser,
|
||||||
|
session: Session,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Form(form): Form<ProcessesForm>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
use crate::config::ProcessConfig;
|
||||||
|
let names = form.proc_name.unwrap_or_default();
|
||||||
|
let patterns = form.proc_pattern.unwrap_or_default();
|
||||||
|
let mem_thresholds = form.proc_mem_threshold.unwrap_or_default();
|
||||||
|
let enableds = form.proc_enabled.unwrap_or_default();
|
||||||
|
let alert_downs = form.proc_alert_down.unwrap_or_default();
|
||||||
|
|
||||||
|
let mut processes = Vec::new();
|
||||||
|
for (i, name) in names.iter().enumerate() {
|
||||||
|
let name = name.trim().to_string();
|
||||||
|
if name.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
processes.push(ProcessConfig {
|
||||||
|
name,
|
||||||
|
pattern: patterns
|
||||||
|
.get(i)
|
||||||
|
.map(|s| s.trim().to_lowercase())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
memory_threshold_mb: mem_thresholds
|
||||||
|
.get(i)
|
||||||
|
.and_then(|s| s.parse().ok())
|
||||||
|
.unwrap_or(0.0),
|
||||||
|
enabled: enableds.contains(&i.to_string()),
|
||||||
|
alert_on_down: alert_downs.contains(&i.to_string()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mut cm = state.config_manager.lock().await;
|
||||||
|
cm.config.processes = processes;
|
||||||
|
cm.save();
|
||||||
|
}
|
||||||
|
flash(&session, "success", "Processus surveillés mis à jour.").await;
|
||||||
|
Redirect::to("/settings")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct PasswordForm {
|
||||||
|
pub current_password: String,
|
||||||
|
pub new_password: String,
|
||||||
|
pub confirm_password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_password(
|
||||||
|
_auth: AuthUser,
|
||||||
|
session: Session,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Form(form): Form<PasswordForm>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let hash = {
|
||||||
|
let cm = state.config_manager.lock().await;
|
||||||
|
cm.config.admin.password_hash.clone()
|
||||||
|
};
|
||||||
|
if !bcrypt::verify(&form.current_password, &hash).unwrap_or(false) {
|
||||||
|
flash(&session, "danger", "Mot de passe actuel incorrect.").await;
|
||||||
|
return Redirect::to("/settings");
|
||||||
|
}
|
||||||
|
if form.new_password.len() < 8 {
|
||||||
|
flash(
|
||||||
|
&session,
|
||||||
|
"danger",
|
||||||
|
"Le nouveau mot de passe doit faire au moins 8 caractères.",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return Redirect::to("/settings");
|
||||||
|
}
|
||||||
|
if form.new_password != form.confirm_password {
|
||||||
|
flash(
|
||||||
|
&session,
|
||||||
|
"danger",
|
||||||
|
"Les mots de passe ne correspondent pas.",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return Redirect::to("/settings");
|
||||||
|
}
|
||||||
|
let new_hash = match bcrypt::hash(&form.new_password, bcrypt::DEFAULT_COST) {
|
||||||
|
Ok(h) => h,
|
||||||
|
Err(_) => {
|
||||||
|
flash(
|
||||||
|
&session,
|
||||||
|
"danger",
|
||||||
|
"Erreur lors du hachage du mot de passe.",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return Redirect::to("/settings");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
{
|
||||||
|
let mut cm = state.config_manager.lock().await;
|
||||||
|
cm.config.admin.password_hash = new_hash;
|
||||||
|
cm.save();
|
||||||
|
}
|
||||||
|
flash(&session, "success", "Mot de passe mis à jour.").await;
|
||||||
|
Redirect::to("/settings")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct PortForm {
|
||||||
|
pub port: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_port(
|
||||||
|
_auth: AuthUser,
|
||||||
|
session: Session,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Form(form): Form<PortForm>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
if !(1024..=65535).contains(&form.port) {
|
||||||
|
flash(
|
||||||
|
&session,
|
||||||
|
"danger",
|
||||||
|
"Le port doit être entre 1024 et 65535.",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return Redirect::to("/settings");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mut cm = state.config_manager.lock().await;
|
||||||
|
cm.config.port = form.port;
|
||||||
|
cm.save();
|
||||||
|
}
|
||||||
|
flash(
|
||||||
|
&session,
|
||||||
|
"warning",
|
||||||
|
&format!(
|
||||||
|
"Port mis à jour à {}. Redémarrez l'application pour appliquer.",
|
||||||
|
form.port
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
Redirect::to("/settings")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct AmadeaLogPathForm {
|
||||||
|
pub amadea_log_path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_amadea_log_path(
|
||||||
|
_auth: AuthUser,
|
||||||
|
session: Session,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Form(form): Form<AmadeaLogPathForm>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let path = form.amadea_log_path.trim().to_string();
|
||||||
|
if path.is_empty() {
|
||||||
|
flash(&session, "danger", "Le chemin ne peut pas être vide.").await;
|
||||||
|
return Redirect::to("/settings");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mut cm = state.config_manager.lock().await;
|
||||||
|
cm.config.amadea_log_path = path;
|
||||||
|
cm.save();
|
||||||
|
}
|
||||||
|
flash(&session, "success", "Chemin des logs Amadea mis à jour.").await;
|
||||||
|
Redirect::to("/settings")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct UserThresholdsForm {
|
||||||
|
pub active_minutes: u64,
|
||||||
|
pub inactive_minutes: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_user_thresholds(
|
||||||
|
_auth: AuthUser,
|
||||||
|
session: Session,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Form(form): Form<UserThresholdsForm>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
if form.active_minutes < 1 || form.inactive_minutes < 1 {
|
||||||
|
flash(
|
||||||
|
&session,
|
||||||
|
"danger",
|
||||||
|
"Les seuils doivent être d'au moins 1 minute.",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return Redirect::to("/settings");
|
||||||
|
}
|
||||||
|
if form.active_minutes >= form.inactive_minutes {
|
||||||
|
flash(
|
||||||
|
&session,
|
||||||
|
"danger",
|
||||||
|
"Le seuil 'actif' doit être inférieur au seuil 'inactif'.",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return Redirect::to("/settings");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mut cm = state.config_manager.lock().await;
|
||||||
|
cm.config.user_status_thresholds.active_minutes = form.active_minutes;
|
||||||
|
cm.config.user_status_thresholds.inactive_minutes = form.inactive_minutes;
|
||||||
|
cm.save();
|
||||||
|
}
|
||||||
|
flash(&session, "success", "Seuils utilisateurs mis à jour.").await;
|
||||||
|
Redirect::to("/settings")
|
||||||
|
}
|
||||||
|
|||||||
@@ -1 +1,58 @@
|
|||||||
// Users routes — Task 11
|
use axum::{
|
||||||
|
extract::State,
|
||||||
|
response::{IntoResponse, Json},
|
||||||
|
};
|
||||||
|
use serde_json::json;
|
||||||
|
use tower_sessions::Session;
|
||||||
|
|
||||||
|
use crate::routes::{get_and_clear_flash, render_html, AppState, AuthUser};
|
||||||
|
|
||||||
|
pub async fn users_get(
|
||||||
|
_auth: AuthUser,
|
||||||
|
session: Session,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let flash_messages = get_and_clear_flash(&session).await;
|
||||||
|
let mut ctx = tera::Context::new();
|
||||||
|
ctx.insert("flash_messages", &flash_messages);
|
||||||
|
ctx.insert("is_authenticated", &true);
|
||||||
|
ctx.insert("active_page", "users");
|
||||||
|
render_html(&state.tera, "users.html", ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn api_users(
|
||||||
|
_auth: AuthUser,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let data = state.user_monitor.data.lock().unwrap().clone();
|
||||||
|
if let Some(err) = &data.error {
|
||||||
|
return Json(json!({ "error": err }));
|
||||||
|
}
|
||||||
|
if data.no_files {
|
||||||
|
return Json(json!({ "no_files": true }));
|
||||||
|
}
|
||||||
|
let users: Vec<serde_json::Value> = data
|
||||||
|
.users
|
||||||
|
.iter()
|
||||||
|
.map(|u| {
|
||||||
|
json!({
|
||||||
|
"login": u.login,
|
||||||
|
"status": u.status,
|
||||||
|
"last_action_time": u.last_action_time.format("%H:%M:%S").to_string(),
|
||||||
|
"last_action_label": u.last_action_label,
|
||||||
|
"action_count_24h": u.action_count_24h,
|
||||||
|
"connected_since": u.connected_since.map(|t| t.format("%H:%M").to_string()),
|
||||||
|
"explicit_logout": u.explicit_logout,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Json(json!({ "users": users, "hourly": data.hourly }))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn api_users_weekly(
|
||||||
|
_auth: AuthUser,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let weekly = state.user_monitor.get_weekly_activity().await;
|
||||||
|
Json(json!({ "weekly": weekly }))
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user