Files
supervision/src/routes/settings.rs
2026-04-27 12:03:08 +02:00

401 lines
11 KiB
Rust

use axum::{
body::Bytes,
extract::{Form, State},
response::{IntoResponse, Redirect},
};
use serde::Deserialize;
use tower_sessions::Session;
use form_urlencoded;
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")
}
pub async fn update_processes(
_auth: AuthUser,
session: Session,
State(state): State<AppState>,
body: Bytes,
) -> impl IntoResponse {
use crate::config::ProcessConfig;
let mut names: Vec<String> = Vec::new();
let mut patterns: Vec<String> = Vec::new();
let mut mem_thresholds: Vec<String> = Vec::new();
let mut enableds: Vec<String> = Vec::new();
let mut alert_downs: Vec<String> = Vec::new();
for (key, value) in form_urlencoded::parse(body.as_ref()) {
match key.as_ref() {
"proc_name[]" => names.push(value.into_owned()),
"proc_pattern[]" => patterns.push(value.into_owned()),
"proc_mem_threshold[]" => mem_thresholds.push(value.into_owned()),
"proc_enabled[]" => enableds.push(value.into_owned()),
"proc_alert_down[]" => alert_downs.push(value.into_owned()),
_ => {}
}
}
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 pause_threshold_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 || form.pause_threshold_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.config.user_status_thresholds.pause_threshold_minutes = form.pause_threshold_minutes;
cm.save();
}
flash(&session, "success", "Seuils utilisateurs mis à jour.").await;
Redirect::to("/settings")
}