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 alerts;
|
||||
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