diff --git a/README.md b/README.md index 6187960..f174fab 100644 --- a/README.md +++ b/README.md @@ -171,7 +171,7 @@ Here is the matrix of deployed services: | | Filebrowser | 8002 (Localhost) | `http://localhost:8002` | Web File Manager | | | Yourls | 8003 (Localhost) | `http://localhost:8003` | URL Shortener | | | GLPI | 8088 (Localhost) | `http://localhost:8088` | IT Asset Management | -| | Gitea | 3000 (Localhost) | `http://localhost:3000` | Self-hosted Git | +| | Gitea | 3000 (Localhost), 2222 | `http://localhost:3000` | Self-hosted Git | | | Roundcube | 8090 (Localhost) | `http://localhost:8090` | Webmail | | | Mailserver | 25, 143, 587, 993 | - | Full Mail Server | | | Syncthing | 8384 (Localhost), 22000 | `http://localhost:8384` | File Synchronization | @@ -325,7 +325,7 @@ Voici la matrice des services déployés : | | Filebrowser | 8002 (Localhost) | `http://localhost:8002` | Web File Manager | | | Yourls | 8003 (Localhost) | `http://localhost:8003` | URL Shortener | | | GLPI | 8088 (Localhost) | `http://localhost:8088` | IT Asset Management | -| | Gitea | 3000 (Localhost) | `http://localhost:3000` | Self-hosted Git | +| | Gitea | 3000 (Localhost), 2222 | `http://localhost:3000` | Self-hosted Git | | | Roundcube | 8090 (Localhost) | `http://localhost:8090` | Webmail | | | Mailserver | 25, 143, 587, 993 | - | Full Mail Server | | | Syncthing | 8384 (Localhost), 22000 | `http://localhost:8384` | Synchronisation de Fichiers | diff --git a/server_manager/src/core/secrets.rs b/server_manager/src/core/secrets.rs index 5908693..cb4c15b 100644 --- a/server_manager/src/core/secrets.rs +++ b/server_manager/src/core/secrets.rs @@ -1,7 +1,7 @@ use anyhow::{Context, Result}; use log::info; -use serde::{Deserialize, Serialize}; use rand::RngExt; +use serde::{Deserialize, Serialize}; use std::fs; use std::path::Path; diff --git a/server_manager/src/core/users.rs b/server_manager/src/core/users.rs index 41870d0..b4f79a8 100644 --- a/server_manager/src/core/users.rs +++ b/server_manager/src/core/users.rs @@ -233,7 +233,9 @@ mod tests { // Verify let user = manager.verify("testuser", "password123"); assert!(user.is_some()); - assert_eq!(user.unwrap().role, Role::Observer); + if let Some(u) = user { + assert_eq!(u.role, Role::Observer); + } assert!(manager.verify("testuser", "wrongpass").is_none()); @@ -250,17 +252,16 @@ mod tests { #[test] fn test_admin_protection() { let mut manager = UserManager::default(); - manager - .add_user("admin", "admin", Role::Admin, None) - .unwrap(); + let res1 = manager.add_user("admin", "admin", Role::Admin, None); + assert!(res1.is_ok()); // Should fail to delete last admin assert!(manager.delete_user("admin").is_err()); // Add another admin - manager - .add_user("admin2", "admin", Role::Admin, None) - .unwrap(); + let res2 = manager.add_user("admin2", "admin", Role::Admin, None); + assert!(res2.is_ok()); + // Now can delete one assert!(manager.delete_user("admin").is_ok()); } diff --git a/server_manager/src/interface/cli.rs b/server_manager/src/interface/cli.rs index bdadad9..35e78e1 100644 --- a/server_manager/src/interface/cli.rs +++ b/server_manager/src/interface/cli.rs @@ -252,32 +252,98 @@ async fn run_install() -> Result<()> { fn print_deployment_summary(secrets: &secrets::Secrets) { let mut summary = String::new(); - summary.push_str("\n=================================================================================\n"); + summary.push_str( + "\n=================================================================================\n", + ); summary.push_str(" DEPLOYMENT SUMMARY 🚀\n"); - summary.push_str("=================================================================================\n"); - summary.push_str(&format!("{:<15} | {:<25} | {:<15} | Password / Info\n", "Service", "URL", "User")); - summary.push_str(&format!("{:<15} | {:<25} | {:<15} | ---------------\n", "-------", "---", "----")); + summary.push_str( + "=================================================================================\n", + ); + let _ = std::fmt::Write::write_fmt( + &mut summary, + format_args!( + "{:<15} | {:<25} | {:<15} | Password / Info\n", + "Service", "URL", "User" + ), + ); + let _ = std::fmt::Write::write_fmt( + &mut summary, + format_args!( + "{:<15} | {:<25} | {:<15} | ---------------\n", + "-------", "---", "----" + ), + ); let mut append_row = |service: &str, url: &str, user: &str, pass: &str| { - summary.push_str(&format!("{:<15} | {:<25} | {:<15} | {}\n", service, url, user, pass)); + let _ = std::fmt::Write::write_fmt( + &mut summary, + format_args!("{:<15} | {:<25} | {:<15} | {}\n", service, url, user, pass), + ); }; // Helper to format Option let pass = |opt: &Option| opt.clone().unwrap_or_else(|| "ERROR".to_string()); - append_row("Nginx Proxy", "http://:81", "admin@example.com", "changeme"); - append_row("Portainer", "http://:9000", "admin", "Set on first login"); - append_row("Nextcloud", "https://:4443", "admin", &pass(&secrets.nextcloud_admin_password)); - append_row("Vaultwarden", "http://:8001/admin", "(Token)", &pass(&secrets.vaultwarden_admin_token)); + append_row( + "Nginx Proxy", + "http://:81", + "admin@example.com", + "changeme", + ); + append_row( + "Portainer", + "http://:9000", + "admin", + "Set on first login", + ); + append_row( + "Nextcloud", + "https://:4443", + "admin", + &pass(&secrets.nextcloud_admin_password), + ); + append_row( + "Vaultwarden", + "http://:8001/admin", + "(Token)", + &pass(&secrets.vaultwarden_admin_token), + ); append_row("Gitea", "http://:3000", "Register", "DB pre-configured"); - append_row("GLPI", "http://:8088", "glpi", "glpi (Change immediately!)"); - append_row("Yourls", "http://:8003/admin", "admin", &pass(&secrets.yourls_admin_password)); - append_row("Roundcube", "http://:8090", "-", "Login with Mail creds"); - append_row("MailServer", "PORTS: 25, 143...", "CLI", "docker exec -ti mailserver setup ..."); + append_row( + "GLPI", + "http://:8088", + "glpi", + "glpi (Change immediately!)", + ); + append_row( + "Yourls", + "http://:8003/admin", + "admin", + &pass(&secrets.yourls_admin_password), + ); + append_row( + "Roundcube", + "http://:8090", + "-", + "Login with Mail creds", + ); + append_row( + "MailServer", + "PORTS: 25, 143...", + "CLI", + "docker exec -ti mailserver setup ...", + ); append_row("Plex", "http://:32400/web", "-", "Follow Web Setup"); - append_row("ArrStack", "http://:8989 (Sonarr)", "-", "No auth by default"); - - summary.push_str("=================================================================================\n\n"); + append_row( + "ArrStack", + "http://:8989 (Sonarr)", + "-", + "No auth by default", + ); + + summary.push_str( + "=================================================================================\n\n", + ); summary.push_str("NOTE: Replace with your server's IP address."); println!("{}", summary); diff --git a/server_manager/src/interface/web.rs b/server_manager/src/interface/web.rs index 1f677c3..b5c2c42 100644 --- a/server_manager/src/interface/web.rs +++ b/server_manager/src/interface/web.rs @@ -90,7 +90,8 @@ impl AppState { let file_path = if path.exists() { path } else { fallback_path }; // Fast path: check metadata - let current_mtime = tokio::fs::metadata(file_path).await + let current_mtime = tokio::fs::metadata(file_path) + .await .and_then(|m| m.modified()) .ok(); @@ -106,7 +107,8 @@ impl AppState { let mut cache = self.users_cache.write().await; // Re-check mtime under write lock - let current_mtime_2 = tokio::fs::metadata(file_path).await + let current_mtime_2 = tokio::fs::metadata(file_path) + .await .and_then(|m| m.modified()) .ok(); @@ -226,11 +228,18 @@ struct LoginPayload { password: String, } -async fn login_handler(State(state): State, session: Session, Form(payload): Form) -> impl IntoResponse { +async fn login_handler( + State(state): State, + session: Session, + Form(payload): Form, +) -> impl IntoResponse { // Reload users on login attempt to get fresh data let user_manager = state.get_users().await; - if let Some(user) = user_manager.verify_async(&payload.username, &payload.password).await { + if let Some(user) = user_manager + .verify_async(&payload.username, &payload.password) + .await + { let session_user = SessionUser { username: user.username, role: user.role, @@ -277,7 +286,9 @@ impl<'a> std::fmt::Display for Escaped<'a> { // Helper for common HTML head fn write_html_head(out: &mut String, title: &str) { - let _ = write!(out, r#" + let _ = write!( + out, + r#" @@ -307,15 +318,19 @@ fn write_html_head(out: &mut String, title: &str) {
- "#, title); + "#, + title + ); } fn write_html_foot(out: &mut String) { - out.push_str(r#" + out.push_str( + r#"
- "#); + "#, + ); } // Protected Dashboard @@ -331,9 +346,15 @@ async fn dashboard(State(state): State, session: Session) -> impl I let config = state.get_config().await; // System Stats - let mut sys = state.system.lock().unwrap(); + let mut sys = state + .system + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); let now = SystemTime::now(); - let mut last_refresh = state.last_system_refresh.lock().unwrap(); + let mut last_refresh = state + .last_system_refresh + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); // Throttle refresh to max once every 500ms if now @@ -365,7 +386,8 @@ async fn dashboard(State(state): State, session: Session) -> impl I if let Some(disk) = target_disk { disk_total = disk.total_space() / 1024 / 1024 / 1024; // GB - disk_used = (disk.total_space() - disk.available_space()) / 1024 / 1024 / 1024; // GB + disk_used = (disk.total_space() - disk.available_space()) / 1024 / 1024 / 1024; + // GB } drop(sys); // Release lock explicitely @@ -381,7 +403,9 @@ async fn dashboard(State(state): State, session: Session) -> impl I - "#, Escaped(&user.username)); + "#, + Escaped(&user.username) + ); // Navigation if is_admin { @@ -488,7 +512,8 @@ async fn dashboard(State(state): State, session: Session) -> impl I

Note: Actions may take a moment to apply.

- "#); + "#, + ); write_html_foot(&mut html); Html(html).into_response() @@ -581,14 +606,18 @@ async fn users_page(State(state): State, session: Session) -> impl // Don't allow deleting self or last admin logic is handled in delete handler/manager // But let's show delete button generally - let _ = write!(html, r#" + let _ = write!( + html, + r#"
- "#, Escaped(&u.username)); + "#, + Escaped(&u.username) + ); } html.push_str(""); @@ -605,7 +634,11 @@ struct AddUserPayload { quota: Option, } -async fn add_user_handler(State(state): State, session: Session, Form(payload): Form) -> impl IntoResponse { +async fn add_user_handler( + State(state): State, + session: Session, + Form(payload): Form, +) -> impl IntoResponse { let session_user: SessionUser = match session.get(SESSION_KEY).await { Ok(Some(u)) => u, _ => return Redirect::to("/login").into_response(), @@ -628,27 +661,36 @@ async fn add_user_handler(State(state): State, session: Session, Fo let mut cache = state.users_cache.write().await; let res = tokio::task::block_in_place(|| { - cache.manager.add_user(&payload.username, &payload.password, role_enum, quota_val) + cache + .manager + .add_user(&payload.username, &payload.password, role_enum, quota_val) }); if let Err(e) = res { error!("Failed to add user: {}", e); // In a real app we'd flash a message. Here just redirect. } else { - info!("User {} added via Web UI by {}", payload.username, session_user.username); + info!( + "User {} added via Web UI by {}", + payload.username, session_user.username + ); // Update mtime to prevent unnecessary reload let path = std::path::Path::new("users.yaml"); let fallback_path = std::path::Path::new("/opt/server_manager/users.yaml"); let file_path = if path.exists() { path } else { fallback_path }; if let Ok(m) = std::fs::metadata(file_path) { - cache.last_modified = m.modified().ok(); + cache.last_modified = m.modified().ok(); } } Redirect::to("/users").into_response() } -async fn delete_user_handler(State(state): State, session: Session, Path(username): Path) -> impl IntoResponse { +async fn delete_user_handler( + State(state): State, + session: Session, + Path(username): Path, +) -> impl IntoResponse { let session_user: SessionUser = match session.get(SESSION_KEY).await { Ok(Some(u)) => u, _ => return Redirect::to("/login").into_response(), @@ -659,20 +701,21 @@ async fn delete_user_handler(State(state): State, session: Session, } let mut cache = state.users_cache.write().await; - let res = tokio::task::block_in_place(|| { - cache.manager.delete_user(&username) - }); + let res = tokio::task::block_in_place(|| cache.manager.delete_user(&username)); if let Err(e) = res { error!("Failed to delete user: {}", e); } else { - info!("User {} deleted via Web UI by {}", username, session_user.username); - // Update mtime to prevent unnecessary reload + info!( + "User {} deleted via Web UI by {}", + username, session_user.username + ); + // Update mtime to prevent unnecessary reload let path = std::path::Path::new("users.yaml"); let fallback_path = std::path::Path::new("/opt/server_manager/users.yaml"); let file_path = if path.exists() { path } else { fallback_path }; if let Ok(m) = std::fs::metadata(file_path) { - cache.last_modified = m.modified().ok(); + cache.last_modified = m.modified().ok(); } } diff --git a/server_manager/src/services/infra.rs b/server_manager/src/services/infra.rs index 3b67947..8f89848 100644 --- a/server_manager/src/services/infra.rs +++ b/server_manager/src/services/infra.rs @@ -30,30 +30,39 @@ impl Service for MariaDBService { // Nextcloud if let Some(pass) = &secrets.nextcloud_db_password { sql.push_str("CREATE DATABASE IF NOT EXISTS nextcloud;\n"); - sql.push_str(&format!( - "CREATE USER IF NOT EXISTS 'nextcloud'@'%' IDENTIFIED BY '{}';\n", - escape(pass) - )); + let _ = std::fmt::Write::write_fmt( + &mut sql, + format_args!( + "CREATE USER IF NOT EXISTS 'nextcloud'@'%' IDENTIFIED BY '{}';\n", + escape(pass) + ), + ); sql.push_str("GRANT ALL PRIVILEGES ON nextcloud.* TO 'nextcloud'@'%';\n"); } // GLPI if let Some(pass) = &secrets.glpi_db_password { sql.push_str("CREATE DATABASE IF NOT EXISTS glpi;\n"); - sql.push_str(&format!( - "CREATE USER IF NOT EXISTS 'glpi'@'%' IDENTIFIED BY '{}';\n", - escape(pass) - )); + let _ = std::fmt::Write::write_fmt( + &mut sql, + format_args!( + "CREATE USER IF NOT EXISTS 'glpi'@'%' IDENTIFIED BY '{}';\n", + escape(pass) + ), + ); sql.push_str("GRANT ALL PRIVILEGES ON glpi.* TO 'glpi'@'%';\n"); } // Gitea if let Some(pass) = &secrets.gitea_db_password { sql.push_str("CREATE DATABASE IF NOT EXISTS gitea;\n"); - sql.push_str(&format!( - "CREATE USER IF NOT EXISTS 'gitea'@'%' IDENTIFIED BY '{}';\n", - escape(pass) - )); + let _ = std::fmt::Write::write_fmt( + &mut sql, + format_args!( + "CREATE USER IF NOT EXISTS 'gitea'@'%' IDENTIFIED BY '{}';\n", + escape(pass) + ), + ); sql.push_str("GRANT ALL PRIVILEGES ON gitea.* TO 'gitea'@'%';\n"); }