From 382494951c4999e15805be026a0f6b6f9130a082 Mon Sep 17 00:00:00 2001 From: Aditya Date: Sat, 4 Jul 2026 00:04:14 +0530 Subject: [PATCH 1/6] feat(security): implement setup key verification and document v1.1.0 changes --- CHANGELOG.md | 40 +++++++++++++++++ README.md | 3 +- backend/src/lib.rs | 5 +++ backend/src/main.rs | 7 +++ backend/src/setup.rs | 86 ++++++++++++++++++++++++++++++++++++ backend/tests/integration.rs | 24 +++++++++- 6 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e7cc323 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,40 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.1.0] - 2026-07-04 + +### Added +- **Setup Key Verification**: Implemented backend validation for the console-printed Setup Key (via new endpoints `/api/setup/check-token` and `/api/setup/verify-setup-token`) and cookie-based authorization to secure the Setup Wizard against unauthorized remote access. +- **Startup Console**: Added descriptive instructions and use-case details about the generated setup key in the startup console banner. +- **Documentation**: Documented the Setup Key generation and its verification purposes in the main README.md file. + +## [1.0.1] - 2026-07-03 + +### Fixed +- **CI/CD Pipeline**: GitHub Actions build and linker failures on macOS and Linux runners. +- **Linux Platform**: Added missing `libxdo-dev` dependency on build environments to fix input compilation/linking. +- **macOS Platform**: Fixed key mapping error by conditionalizing `F21`–`F24` keys, which are not supported by Enigo on macOS. +- **macOS Platform**: Silenced compiler warning for unused `brightness` variable during macOS-specific display operations. +- **Frontend Code Quality**: Resolved all `oxlint` accessibility and linting warnings: + - Cleaned up unused imports and variables across multiple components. + - Linked form controls to labels for better screen reader compatibility. + - Added missing `aria-label` properties to toggle elements. + - Normalized React Hook dependency arrays. +- **Backend Code Quality**: Removed unused imports (`std::io`, `std::ffi::c_void`), cleaned up unused `mut` specifiers, and resolved `Arc` borrow-after-move error in `main.rs`. + +## [1.0.0] - 2026-07-03 + +### Added +- **Core Architecture**: Lightweight remote system administration agent utilizing a Rust backend and embedded React/TypeScript single-page app (SPA). +- **System Telemetry**: Real-time telemetry monitoring including CPU load, RAM usage, network interface speeds, disk usage, and battery capacity. +- **File Manager**: Directory navigation, file downloads, secure path validation blocklists, and streaming uploads supporting files up to 500MB. +- **Terminal Emulator**: Remote interactive terminal interface leveraging PTY and `xterm.js`. +- **Script Engine**: Secure execution of PowerShell (Windows) and Bash (macOS/Linux) scripts with customizable execution timeouts. +- **Power Management**: Support for remote Shutdown, Restart, Sleep, Hibernate, Sign Out, Lock, and Switch User actions with a 5-second grace period cancel queue. +- **Cloudflare Tunnels**: Integrated `cloudflared` tunnel creation for secure remote access without port forwarding. +- **System Tray Widget**: Native system tray application displaying active status, quick tunnel actions, and port configurations. +- **Security & MFA**: Cryptographic password hashing (Argon2id), mandatory TOTP multi-factor authentication setup, recovery code generation, and automated lockout policies. diff --git a/README.md b/README.md index 60583bb..6de0d72 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,8 @@ On first run, the backend: 2. Stores the JWT signing key in your OS keychain (Credential Manager / Keychain / Secret Service) 3. Downloads the correct `cloudflared` binary for your platform with SHA256 verification 4. Binds to `localhost:3939` (falls back to random port) -5. Opens your browser to the setup wizard +5. Generates a one-time **Setup Key** printed to the console window. This key acts as a security token to verify physical/owner access to the host machine. +6. Opens your browser to the first-run Setup Wizard, where you must enter this Setup Key to unlock configuration steps (admin credentials, TOTP, and remote access relay). ### Production Build diff --git a/backend/src/lib.rs b/backend/src/lib.rs index 3e0c408..eae7c17 100644 --- a/backend/src/lib.rs +++ b/backend/src/lib.rs @@ -572,6 +572,11 @@ pub fn build_router(state: AppState) -> Router { .route("/ws", get(ws::ws_handler)) .route("/api/telemetry/history", get(history_handler)) .route("/api/setup/status", get(setup::setup_status_handler)) + .route("/api/setup/check-token", get(setup::check_token_handler)) + .route( + "/api/setup/verify-setup-token", + post(setup::verify_setup_token_handler), + ) .route("/api/setup/password", post(setup::api_password_handler)) .route("/api/setup/totp", post(setup::api_totp_handler)) .route( diff --git a/backend/src/main.rs b/backend/src/main.rs index 91c53e2..9295828 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -418,6 +418,7 @@ async fn main() { println!(" Manage → System tray icon"); println!(" Setup key → {}", setup_token); println!(" ──────────────────────────────────────────────────"); + println!(" (Use the setup key to authorize the first-run configuration wizard)"); let is_headless = cfg!(target_os = "linux") && std::env::var("DISPLAY").is_err(); if is_headless { @@ -425,10 +426,16 @@ async fn main() { println!(" Headless mode — no display detected."); println!(" Use SSH port forwarding to reach the dashboard:"); println!(" ssh -L {}:127.0.0.1:{} user@host", port, port); + println!(); + println!(" Note: Keep the Setup key above handy. When you open the"); + println!(" dashboard, you must enter it to unlock the setup wizard."); } else { println!(); println!(" Press Enter to open the dashboard."); println!(" Close this window at any time — SysDeck keeps running."); + println!(); + println!(" Note: Copy the Setup key above before proceeding. It is"); + println!(" required to authorize the first-run configuration wizard."); } println!(); // ──────────────────────────────────────────────────────────────────────── diff --git a/backend/src/setup.rs b/backend/src/setup.rs index cdb7b57..584361c 100644 --- a/backend/src/setup.rs +++ b/backend/src/setup.rs @@ -69,6 +69,11 @@ pub struct ProgressQuery { pub token: String, } +#[derive(Deserialize)] +pub struct VerifySetupTokenRequest { + pub token: String, +} + #[derive(Serialize)] pub(crate) struct SetupStatus { is_setup_complete: bool, @@ -83,12 +88,93 @@ pub(crate) async fn setup_status_handler(State(state): State) -> Json< Json(SetupStatus { is_setup_complete }) } +pub(crate) async fn check_token_handler(State(state): State) -> Json { + let needs_setup = { + let conn = state.db.lock().await; + db::is_setup_complete(&conn).map(|c| !c).unwrap_or(true) + }; + Json(serde_json::json!({ "token_required": needs_setup })) +} + +pub(crate) async fn verify_setup_token_handler( + State(state): State, + Json(body): Json, +) -> Response { + if body.token == *state.setup_token { + use sha2::{Sha256, Digest}; + let mut hasher = Sha256::new(); + hasher.update(state.setup_token.as_bytes()); + let hash_hex = hasher.finalize().iter().map(|b| format!("{:02x}", b)).collect::(); + + let cookie_val = format!( + "setup_authorized={}; Path=/; HttpOnly; SameSite=Lax; Max-Age=3600", + hash_hex + ); + + Response::builder() + .header(axum::http::header::SET_COOKIE, cookie_val) + .body(axum::body::Body::from( + serde_json::json!({ "success": true }).to_string() + )) + .unwrap() + } else { + Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(axum::body::Body::from( + serde_json::json!({ "success": false, "error": "Invalid setup token" }).to_string() + )) + .unwrap() + } +} + // --- JSON API Handlers --- pub async fn api_password_handler( State(state): State, + headers: axum::http::HeaderMap, Json(body): Json, ) -> Response { + let needs_setup = { + let conn = state.db.lock().await; + db::is_setup_complete(&conn).map(|c| !c).unwrap_or(true) + }; + + if needs_setup { + let is_authorized = headers + .get(axum::http::header::COOKIE) + .and_then(|c| c.to_str().ok()) + .and_then(|c_str| { + c_str.split(';').find_map(|cookie| { + let parts: Vec<&str> = cookie.trim().split('=').collect(); + if parts.len() == 2 && parts[0] == "setup_authorized" { + Some(parts[1]) + } else { + None + } + }) + }) + .map(|cookie_val| { + use sha2::{Sha256, Digest}; + let mut hasher = Sha256::new(); + hasher.update(state.setup_token.as_bytes()); + let expected_hash = hasher.finalize().iter().map(|b| format!("{:02x}", b)).collect::(); + cookie_val == expected_hash + }) + .unwrap_or(false); + + if !is_authorized { + tracing::warn!(handler = "api_password_handler", "unauthorized setup attempt"); + return ( + StatusCode::UNAUTHORIZED, + Json(serde_json::json!({ + "success": false, + "error": "Unauthorized: Setup token verification required" + })), + ) + .into_response(); + } + } + if body.password.len() < 8 { tracing::warn!(handler = "api_password_handler", "password too short"); return (StatusCode::BAD_REQUEST, Json(serde_json::json!({"success": false, "error": "Password must be at least 8 characters"}))).into_response(); diff --git a/backend/tests/integration.rs b/backend/tests/integration.rs index a12f5dc..6e3bf22 100644 --- a/backend/tests/integration.rs +++ b/backend/tests/integration.rs @@ -14,10 +14,21 @@ use tower::ServiceExt; async fn test_setup_json_api_full_flow() { let (mut router, _state) = test_app(); - // Step 1: Set password + // Verify setup token to get setup_authorized cookie let resp = post_json( + &mut router, + "/api/setup/verify-setup-token", + json!({"token": "test-setup-token-123"}), + ) + .await; + assert_eq!(resp.status(), StatusCode::OK); + let cookie = resp.headers().get(axum::http::header::SET_COOKIE).unwrap().to_str().unwrap().to_string(); + + // Step 1: Set password + let resp = authed_post_json( &mut router, "/api/setup/password", + &cookie, json!({"password": "MySecureP@ss1"}), ) .await; @@ -84,9 +95,20 @@ async fn test_setup_invalid_token_returns_error() { #[tokio::test] async fn test_setup_weak_password_rejected() { let (mut router, _state) = test_app(); + // Verify setup token to get setup_authorized cookie let resp = post_json( + &mut router, + "/api/setup/verify-setup-token", + json!({"token": "test-setup-token-123"}), + ) + .await; + assert_eq!(resp.status(), StatusCode::OK); + let cookie = resp.headers().get(axum::http::header::SET_COOKIE).unwrap().to_str().unwrap().to_string(); + + let resp = authed_post_json( &mut router, "/api/setup/password", + &cookie, json!({"password": "weak"}), ) .await; From aabcf8e44e41bc144cf56c9eb29ec28af5fb23b9 Mon Sep 17 00:00:00 2001 From: Aditya Date: Sat, 4 Jul 2026 00:09:55 +0530 Subject: [PATCH 2/6] ci: trigger CI checks on dev branch and format codebase --- .github/workflows/ci.yml | 4 ++-- backend/src/setup.rs | 27 +++++++++++++++++++-------- backend/tests/integration.rs | 16 ++++++++++++++-- 3 files changed, 35 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 94d1b15..fd0e78f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI Checks on: push: - branches: [ main ] + branches: [ main, dev ] pull_request: - branches: [ main ] + branches: [ main, dev ] env: CARGO_TERM_COLOR: always diff --git a/backend/src/setup.rs b/backend/src/setup.rs index 584361c..b14cf6b 100644 --- a/backend/src/setup.rs +++ b/backend/src/setup.rs @@ -101,27 +101,31 @@ pub(crate) async fn verify_setup_token_handler( Json(body): Json, ) -> Response { if body.token == *state.setup_token { - use sha2::{Sha256, Digest}; + use sha2::{Digest, Sha256}; let mut hasher = Sha256::new(); hasher.update(state.setup_token.as_bytes()); - let hash_hex = hasher.finalize().iter().map(|b| format!("{:02x}", b)).collect::(); + let hash_hex = hasher + .finalize() + .iter() + .map(|b| format!("{:02x}", b)) + .collect::(); let cookie_val = format!( "setup_authorized={}; Path=/; HttpOnly; SameSite=Lax; Max-Age=3600", hash_hex ); - + Response::builder() .header(axum::http::header::SET_COOKIE, cookie_val) .body(axum::body::Body::from( - serde_json::json!({ "success": true }).to_string() + serde_json::json!({ "success": true }).to_string(), )) .unwrap() } else { Response::builder() .status(StatusCode::BAD_REQUEST) .body(axum::body::Body::from( - serde_json::json!({ "success": false, "error": "Invalid setup token" }).to_string() + serde_json::json!({ "success": false, "error": "Invalid setup token" }).to_string(), )) .unwrap() } @@ -154,16 +158,23 @@ pub async fn api_password_handler( }) }) .map(|cookie_val| { - use sha2::{Sha256, Digest}; + use sha2::{Digest, Sha256}; let mut hasher = Sha256::new(); hasher.update(state.setup_token.as_bytes()); - let expected_hash = hasher.finalize().iter().map(|b| format!("{:02x}", b)).collect::(); + let expected_hash = hasher + .finalize() + .iter() + .map(|b| format!("{:02x}", b)) + .collect::(); cookie_val == expected_hash }) .unwrap_or(false); if !is_authorized { - tracing::warn!(handler = "api_password_handler", "unauthorized setup attempt"); + tracing::warn!( + handler = "api_password_handler", + "unauthorized setup attempt" + ); return ( StatusCode::UNAUTHORIZED, Json(serde_json::json!({ diff --git a/backend/tests/integration.rs b/backend/tests/integration.rs index 6e3bf22..badfa3c 100644 --- a/backend/tests/integration.rs +++ b/backend/tests/integration.rs @@ -22,7 +22,13 @@ async fn test_setup_json_api_full_flow() { ) .await; assert_eq!(resp.status(), StatusCode::OK); - let cookie = resp.headers().get(axum::http::header::SET_COOKIE).unwrap().to_str().unwrap().to_string(); + let cookie = resp + .headers() + .get(axum::http::header::SET_COOKIE) + .unwrap() + .to_str() + .unwrap() + .to_string(); // Step 1: Set password let resp = authed_post_json( @@ -103,7 +109,13 @@ async fn test_setup_weak_password_rejected() { ) .await; assert_eq!(resp.status(), StatusCode::OK); - let cookie = resp.headers().get(axum::http::header::SET_COOKIE).unwrap().to_str().unwrap().to_string(); + let cookie = resp + .headers() + .get(axum::http::header::SET_COOKIE) + .unwrap() + .to_str() + .unwrap() + .to_string(); let resp = authed_post_json( &mut router, From aec10d0604ab80c00e0f9fde7b62913c6fda3e5a Mon Sep 17 00:00:00 2001 From: Aditya Date: Sat, 4 Jul 2026 00:21:22 +0530 Subject: [PATCH 3/6] fix(windows): resolve empty console window flashing on startup and polling --- CHANGELOG.md | 3 +++ backend/src/hardware.rs | 7 +++--- backend/src/lib.rs | 25 ++++++++++++++++--- backend/src/network.rs | 5 ++-- backend/src/power.rs | 54 ++++++++++++++++------------------------ backend/src/process.rs | 2 +- backend/src/script.rs | 15 ++++++----- backend/src/telemetry.rs | 2 +- backend/src/tunnel.rs | 3 +-- 9 files changed, 62 insertions(+), 54 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7cc323..f412327 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Startup Console**: Added descriptive instructions and use-case details about the generated setup key in the startup console banner. - **Documentation**: Documented the Setup Key generation and its verification purposes in the main README.md file. +### Fixed +- **Windows Console Flashing**: Resolved an issue where multiple empty Command Prompt/PowerShell terminal windows flashed on the screen on Windows during startup and periodic background operations (telemetry WMI polling, network/hardware status checks, registry queries, and Cloudflare tunnel spawning) by forcing child processes to spawn silently with the `CREATE_NO_WINDOW` process creation flag. + ## [1.0.1] - 2026-07-03 ### Fixed diff --git a/backend/src/hardware.rs b/backend/src/hardware.rs index d0e7004..8cbe263 100644 --- a/backend/src/hardware.rs +++ b/backend/src/hardware.rs @@ -2,7 +2,6 @@ use axum::extract::State; use axum::response::{IntoResponse, Json}; use serde::{Deserialize, Serialize}; use serde_json::json; -use std::process::Command; use std::sync::atomic::Ordering; use std::time::Duration; @@ -82,7 +81,7 @@ pub struct ScheduledPowerRequest { #[allow(dead_code)] fn run_cmd(cmd: &str, args: &[&str]) -> Result { - let output = Command::new(cmd) + let output = crate::new_command(cmd) .args(args) .output() .map_err(|e| format!("Failed to execute {}: {}", cmd, e))?; @@ -99,7 +98,7 @@ fn run_powershell(script: &str) -> Result { use std::io::Write; use std::process::Stdio; - let mut child = Command::new("powershell") + let mut child = crate::new_command("powershell") .args(["-NoProfile", "-Command", "-"]) .stdin(Stdio::piped()) .stdout(Stdio::piped()) @@ -1130,7 +1129,7 @@ pub async fn schedule_power_handler( crate::power::PowerAction::Restart => "/r", _ => "/s", }; - let _ = Command::new("shutdown") + let _ = crate::new_command("shutdown") .args([flag, "/f", "/t", "1"]) .spawn(); } else { diff --git a/backend/src/lib.rs b/backend/src/lib.rs index eae7c17..6b60fcb 100644 --- a/backend/src/lib.rs +++ b/backend/src/lib.rs @@ -79,6 +79,25 @@ pub struct AppState { pub setup_token: Arc, } +pub fn new_command>(program: S) -> std::process::Command { + let mut cmd = std::process::Command::new(program); + #[cfg(target_os = "windows")] + { + use std::os::windows::process::CommandExt; + cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW + } + cmd +} + +pub fn new_tokio_command>(program: S) -> tokio::process::Command { + let mut cmd = tokio::process::Command::new(program); + #[cfg(target_os = "windows")] + { + cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW + } + cmd +} + pub fn get_data_dir() -> PathBuf { #[cfg(target_os = "windows")] { @@ -212,7 +231,7 @@ pub async fn find_available_port() -> (u16, tokio::net::TcpListener) { fn is_startup_enabled() -> bool { #[cfg(target_os = "windows")] { - std::process::Command::new("reg") + new_command("reg") .args([ "query", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", @@ -234,7 +253,7 @@ fn set_startup(enabled: bool) { .map(|p| p.to_string_lossy().to_string()) .unwrap_or_default(); if enabled { - let _ = std::process::Command::new("reg") + let _ = new_command("reg") .args([ "add", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", @@ -248,7 +267,7 @@ fn set_startup(enabled: bool) { ]) .output(); } else { - let _ = std::process::Command::new("reg") + let _ = new_command("reg") .args([ "delete", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", diff --git a/backend/src/network.rs b/backend/src/network.rs index bde8b82..8eef1c8 100644 --- a/backend/src/network.rs +++ b/backend/src/network.rs @@ -1,7 +1,6 @@ use axum::response::{IntoResponse, Json}; use serde::{Deserialize, Serialize}; use serde_json::json; -use std::process::Command; use tracing; #[derive(Serialize, Debug)] @@ -51,7 +50,7 @@ fn default_wifi_security() -> String { } fn run_cmd(cmd: &str, args: &[&str]) -> Result { - let output = Command::new(cmd) + let output = crate::new_command(cmd) .args(args) .output() .map_err(|e| format!("Failed to execute {}: {}", cmd, e))?; @@ -66,7 +65,7 @@ fn run_cmd(cmd: &str, args: &[&str]) -> Result { fn run_powershell(script: &str) -> Result { use std::io::Write; use std::process::Stdio; - let mut child = Command::new("powershell") + let mut child = crate::new_command("powershell") .args(["-NoProfile", "-Command", "-"]) .stdin(Stdio::piped()) .stdout(Stdio::piped()) diff --git a/backend/src/power.rs b/backend/src/power.rs index 239bdb0..39c9f9d 100644 --- a/backend/src/power.rs +++ b/backend/src/power.rs @@ -31,73 +31,63 @@ impl SystemCommands for RealOs { #[cfg(target_os = "windows")] match action { PowerAction::Shutdown => { - let _ = std::process::Command::new("shutdown") + let _ = crate::new_command("shutdown") .args(["/s", "/t", "1"]) .spawn(); } PowerAction::Restart => { - let _ = std::process::Command::new("shutdown") + let _ = crate::new_command("shutdown") .args(["/r", "/t", "1"]) .spawn(); } PowerAction::Sleep => { - let _ = std::process::Command::new("rundll32.exe") + let _ = crate::new_command("rundll32.exe") .args(["powrprof.dll,SetSuspendState", "0", "1", "0"]) .spawn(); } PowerAction::Hibernate => { - let _ = std::process::Command::new("rundll32.exe") + let _ = crate::new_command("rundll32.exe") .args(["powrprof.dll,SetSuspendState", "1", "1", "0"]) .spawn(); } PowerAction::SignOut => { - let _ = std::process::Command::new("shutdown").args(["/l"]).spawn(); + let _ = crate::new_command("shutdown").args(["/l"]).spawn(); } PowerAction::Lock => { - let _ = std::process::Command::new("rundll32.exe") + let _ = crate::new_command("rundll32.exe") .args(["user32.dll,LockWorkStation"]) .spawn(); } PowerAction::SwitchUser => { // Switch to the login screen to allow another user to log in - let _ = std::process::Command::new("tsdiscon.exe").spawn(); + let _ = crate::new_command("tsdiscon.exe").spawn(); } } #[cfg(target_os = "linux")] match action { PowerAction::Shutdown => { - let _ = std::process::Command::new("systemctl") - .arg("poweroff") - .spawn(); + let _ = crate::new_command("systemctl").arg("poweroff").spawn(); } PowerAction::Restart => { - let _ = std::process::Command::new("systemctl") - .arg("reboot") - .spawn(); + let _ = crate::new_command("systemctl").arg("reboot").spawn(); } PowerAction::Sleep => { - let _ = std::process::Command::new("systemctl") - .arg("suspend") - .spawn(); + let _ = crate::new_command("systemctl").arg("suspend").spawn(); } PowerAction::Hibernate => { - let _ = std::process::Command::new("systemctl") - .arg("hibernate") - .spawn(); + let _ = crate::new_command("systemctl").arg("hibernate").spawn(); } PowerAction::SignOut => { - let _ = std::process::Command::new("loginctl") + let _ = crate::new_command("loginctl") .args(["terminate-session", "self"]) .spawn(); } PowerAction::Lock => { - let _ = std::process::Command::new("loginctl") - .arg("lock-session") - .spawn(); + let _ = crate::new_command("loginctl").arg("lock-session").spawn(); } PowerAction::SwitchUser => { // Switch user via display manager - let _ = std::process::Command::new("dm-tool") + let _ = crate::new_command("dm-tool") .arg("switch-to-greeter") .spawn(); } @@ -105,41 +95,41 @@ impl SystemCommands for RealOs { #[cfg(target_os = "macos")] match action { PowerAction::Shutdown => { - let _ = std::process::Command::new("osascript") + let _ = crate::new_command("osascript") .args(["-e", "tell application \"System Events\" to shut down"]) .spawn(); } PowerAction::Restart => { - let _ = std::process::Command::new("osascript") + let _ = crate::new_command("osascript") .args(["-e", "tell application \"System Events\" to restart"]) .spawn(); } PowerAction::Sleep => { - let _ = std::process::Command::new("osascript") + let _ = crate::new_command("osascript") .args(["-e", "tell application \"System Events\" to sleep"]) .spawn(); } PowerAction::Hibernate => { // macOS: enable hibernate mode then sleep - let _ = std::process::Command::new("pmset") + let _ = crate::new_command("pmset") .args(["-a", "hibernatemode", "25"]) .spawn(); - let _ = std::process::Command::new("osascript") + let _ = crate::new_command("osascript") .args(["-e", "tell application \"System Events\" to sleep"]) .spawn(); } PowerAction::SignOut => { - let _ = std::process::Command::new("osascript") + let _ = crate::new_command("osascript") .args(["-e", "tell application \"System Events\" to log out"]) .spawn(); } PowerAction::Lock => { - let _ = std::process::Command::new("/System/Library/CoreServices/Menu Extras/User.menu/Contents/Resources/CGSession") + let _ = crate::new_command("/System/Library/CoreServices/Menu Extras/User.menu/Contents/Resources/CGSession") .arg("-suspend") .spawn(); } PowerAction::SwitchUser => { - let _ = std::process::Command::new("/System/Library/CoreServices/Menu Extras/User.menu/Contents/Resources/CGSession") + let _ = crate::new_command("/System/Library/CoreServices/Menu Extras/User.menu/Contents/Resources/CGSession") .arg("-suspend") .spawn(); } diff --git a/backend/src/process.rs b/backend/src/process.rs index 1e8ca26..f31a12f 100644 --- a/backend/src/process.rs +++ b/backend/src/process.rs @@ -48,7 +48,7 @@ pub async fn kill_handler(Json(body): Json) -> impl IntoResponse { #[cfg(target_os = "windows")] { let pid = body.pid; - match std::process::Command::new("taskkill") + match crate::new_command("taskkill") .args(["/PID", &pid.to_string(), "/F"]) .output() { diff --git a/backend/src/script.rs b/backend/src/script.rs index 70ddbc0..5e12a88 100644 --- a/backend/src/script.rs +++ b/backend/src/script.rs @@ -10,7 +10,6 @@ use axum::response::{IntoResponse, Json, Response}; use serde::{Deserialize, Serialize}; use serde_json::json; use tokio::io::AsyncBufReadExt; -use tokio::process::Command; use tokio::sync::broadcast; use tokio::sync::Mutex; use uuid::Uuid; @@ -19,7 +18,7 @@ fn kill_process_tree(pid: Option) { let Some(pid) = pid else { return }; #[cfg(target_os = "windows")] { - let _ = std::process::Command::new("taskkill") + let _ = crate::new_command("taskkill") .args(["/T", "/F", "/PID", &pid.to_string()]) .spawn() .map(|mut c| { @@ -28,10 +27,10 @@ fn kill_process_tree(pid: Option) { } #[cfg(unix)] { - let _ = std::process::Command::new("pkill") + let _ = crate::new_command("pkill") .args(["-P", &pid.to_string()]) .spawn(); - let _ = std::process::Command::new("kill") + let _ = crate::new_command("kill") .args(["-9", &pid.to_string()]) .spawn(); } @@ -223,7 +222,7 @@ async fn run_script( "$OutputEncoding = [System.Text.Encoding]::UTF8; {}", content ); - Command::new("powershell") + crate::new_tokio_command("powershell") .args(["-NoProfile", "-Command", &ps_content]) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) @@ -232,7 +231,7 @@ async fn run_script( #[cfg(target_os = "windows")] { let content = content.replace("\r\n", " & ").replace('\n', " & "); - Command::new("cmd") + crate::new_tokio_command("cmd") .args(["/C", &content]) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) @@ -250,7 +249,7 @@ async fn run_script( #[cfg(target_os = "windows")] { let content = content.replace("\r\n", " & ").replace('\n', " & "); - Command::new("cmd") + crate::new_tokio_command("cmd") .args(["/C", &content]) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) @@ -258,7 +257,7 @@ async fn run_script( } #[cfg(not(target_os = "windows"))] { - Command::new("bash") + crate::new_tokio_command("bash") .args(["-c", content]) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) diff --git a/backend/src/telemetry.rs b/backend/src/telemetry.rs index d3b71e1..6bc8d66 100644 --- a/backend/src/telemetry.rs +++ b/backend/src/telemetry.rs @@ -60,7 +60,7 @@ fn get_wmi_temperatures() -> (Option, Option) { let mut gpu = None; for (source, cmd) in &queries { tracing::debug!(source, "querying WMI thermal zones"); - let output = match std::process::Command::new("powershell") + let output = match crate::new_command("powershell") .args(["-NoProfile", "-NonInteractive", "-Command", cmd]) .output() { diff --git a/backend/src/tunnel.rs b/backend/src/tunnel.rs index 721d0c4..83c8cb4 100644 --- a/backend/src/tunnel.rs +++ b/backend/src/tunnel.rs @@ -9,7 +9,6 @@ use serde::Serialize; use sha2::{Digest, Sha256}; use tokio::fs; use tokio::io::{AsyncBufReadExt, BufReader}; -use tokio::process::Command; use tokio::sync::{broadcast, Mutex, RwLock}; use crate::AppState; @@ -260,7 +259,7 @@ async fn run_tunnel_loop( None => return, }; - let mut child = match Command::new(&state.exe_path) + let mut child = match crate::new_tokio_command(&state.exe_path) .args([ "tunnel", "--no-autoupdate", From 48386da883591634194a2f08abb0897b086fc6d7 Mon Sep 17 00:00:00 2001 From: Aditya Date: Sat, 4 Jul 2026 00:51:12 +0530 Subject: [PATCH 4/6] feat: implement Zero-Friction Portable Workflow (removed setup key, added welcome wizard step, auto-start tunnel on setup complete) --- CHANGELOG.md | 8 +- README.md | 2 +- backend/src/lib.rs | 6 -- backend/src/main.rs | 77 ++++++----------- backend/src/setup.rs | 108 ++++-------------------- backend/tests/common/mod.rs | 1 - backend/tests/integration.rs | 36 +------- frontend/src/pages/Setup.tsx | 155 ++++++++++++++++------------------- 8 files changed, 120 insertions(+), 273 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f412327..113b4ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.1.0] - 2026-07-04 +## [1.1.0] - Unreleased ### Added -- **Setup Key Verification**: Implemented backend validation for the console-printed Setup Key (via new endpoints `/api/setup/check-token` and `/api/setup/verify-setup-token`) and cookie-based authorization to secure the Setup Wizard against unauthorized remote access. -- **Startup Console**: Added descriptive instructions and use-case details about the generated setup key in the startup console banner. -- **Documentation**: Documented the Setup Key generation and its verification purposes in the main README.md file. +- **Zero-Friction Portable Workflow**: Redesigned the onboarding process by completely removing the setup key requirements. Added a welcoming onboarding screen (Step 0) in the browser before configuring the system. +- **Cloudflare Tunnel Auto-Start**: Configured the Cloudflare tunnel to dynamically start immediately upon completing the setup wizard if the user opts in, removing any need for a server restart. +- **User-Friendly Startup Console**: Updated the server CLI banner on launch to show a clean box-drawing layout directing the user to press Enter to instantly minimize the window to system tray and open their default browser. ### Fixed - **Windows Console Flashing**: Resolved an issue where multiple empty Command Prompt/PowerShell terminal windows flashed on the screen on Windows during startup and periodic background operations (telemetry WMI polling, network/hardware status checks, registry queries, and Cloudflare tunnel spawning) by forcing child processes to spawn silently with the `CREATE_NO_WINDOW` process creation flag. diff --git a/README.md b/README.md index 6de0d72..a7b119b 100644 --- a/README.md +++ b/README.md @@ -265,7 +265,7 @@ sysdeck/ | POST | `/api/setup/password` | Set initial password (step 1) | | POST | `/api/setup/totp` | Generate TOTP secret (step 2) | | POST | `/api/setup/verify-totp` | Verify TOTP code (step 3) | -| POST | `/api/setup/recovery-codes` | Generate recovery codes (step 4) | +| POST | `/api/setup/relay` | Set Cloudflare relay opt-in (step 4) | | POST | `/api/setup/finish` | Complete setup | ### Dashboard & Telemetry diff --git a/backend/src/lib.rs b/backend/src/lib.rs index 6b60fcb..0c42d05 100644 --- a/backend/src/lib.rs +++ b/backend/src/lib.rs @@ -76,7 +76,6 @@ pub struct AppState { pub terminal_state: Arc, pub tunnel_state: Arc, pub port: u16, - pub setup_token: Arc, } pub fn new_command>(program: S) -> std::process::Command { @@ -591,11 +590,6 @@ pub fn build_router(state: AppState) -> Router { .route("/ws", get(ws::ws_handler)) .route("/api/telemetry/history", get(history_handler)) .route("/api/setup/status", get(setup::setup_status_handler)) - .route("/api/setup/check-token", get(setup::check_token_handler)) - .route( - "/api/setup/verify-setup-token", - post(setup::verify_setup_token_handler), - ) .route("/api/setup/password", post(setup::api_password_handler)) .route("/api/setup/totp", post(setup::api_totp_handler)) .route( diff --git a/backend/src/main.rs b/backend/src/main.rs index 9295828..435f05b 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -5,7 +5,6 @@ use std::sync::Arc; -use rand::Rng; use tokio::sync::{broadcast, oneshot, Mutex}; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; @@ -202,14 +201,6 @@ async fn main() { let jwt_key = Arc::new(auth::load_or_create_jwt_key().expect("Failed to load JWT signing key")); println!(" ✓ JWT signing key loaded"); - // Generate setup token for headless/remote setup - let setup_token: String = rand::thread_rng() - .sample_iter(&rand::distributions::Alphanumeric) - .take(16) - .map(char::from) - .collect(); - let setup_token = Arc::new(setup_token); - let (port, listener) = find_available_port().await; println!(" ✓ Server bound to localhost:{}", port); @@ -348,7 +339,6 @@ async fn main() { terminal_state, tunnel_state: tunnel_state.clone(), port, - setup_token: setup_token.clone(), }; let app = build_router(app_state.clone()); @@ -411,56 +401,37 @@ async fn main() { server_ready_rx.await.ok(); - // ── Startup banner (complete) ──────────────────────────────────────────── + // ── Startup banner ──────────────────────────────────────────── println!(); - println!(" ──────────────────────────────────────────────────"); - println!(" Dashboard → http://localhost:{}", port); - println!(" Manage → System tray icon"); - println!(" Setup key → {}", setup_token); - println!(" ──────────────────────────────────────────────────"); - println!(" (Use the setup key to authorize the first-run configuration wizard)"); - - let is_headless = cfg!(target_os = "linux") && std::env::var("DISPLAY").is_err(); - if is_headless { - println!(); - println!(" Headless mode — no display detected."); - println!(" Use SSH port forwarding to reach the dashboard:"); - println!(" ssh -L {}:127.0.0.1:{} user@host", port, port); - println!(); - println!(" Note: Keep the Setup key above handy. When you open the"); - println!(" dashboard, you must enter it to unlock the setup wizard."); - } else { - println!(); - println!(" Press Enter to open the dashboard."); - println!(" Close this window at any time — SysDeck keeps running."); - println!(); - println!(" Note: Copy the Setup key above before proceeding. It is"); - println!(" required to authorize the first-run configuration wizard."); - } + println!("┌────────────────────────────────────────────────────────┐"); + println!("│ ⚡ SysDeck is running! │"); + println!("├────────────────────────────────────────────────────────┤"); + println!("│ The app is now active in your system tray. │"); + println!("│ │"); + println!("│ Press [Enter] to open the setup wizard in your browser│"); + println!("│ Press [Ctrl+C] to stop the app │"); + println!("└────────────────────────────────────────────────────────┘"); println!(); - // ──────────────────────────────────────────────────────────────────────── // Spawn a blocking task to wait for Enter, then open the browser and // detach the splash console. On Windows, if the user closes the window // first (CTRL_CLOSE_EVENT), FreeConsole() closes stdin, read_line() // returns EOF, and open_dashboard_once() is a no-op (already opened). - if !is_headless { - tokio::task::spawn_blocking(move || { - let mut buf = String::new(); - // Returns Ok(0) on EOF (stdin closed by FreeConsole) or Ok(n) on Enter. - let _ = std::io::stdin().read_line(&mut buf); - - #[cfg(windows)] - { - detach_splash_console(); - open_dashboard_once(); - } - #[cfg(not(windows))] - { - let _ = open::that(format!("http://localhost:{}", port)); - } - }); - } + tokio::task::spawn_blocking(move || { + let mut buf = String::new(); + // Returns Ok(0) on EOF (stdin closed by FreeConsole) or Ok(n) on Enter. + let _ = std::io::stdin().read_line(&mut buf); + + #[cfg(windows)] + { + detach_splash_console(); + open_dashboard_once(); + } + #[cfg(not(windows))] + { + let _ = open::that(format!("http://localhost:{}", port)); + } + }); server_handle.await.expect("Server task panicked"); diff --git a/backend/src/setup.rs b/backend/src/setup.rs index b14cf6b..72806ac 100644 --- a/backend/src/setup.rs +++ b/backend/src/setup.rs @@ -69,11 +69,6 @@ pub struct ProgressQuery { pub token: String, } -#[derive(Deserialize)] -pub struct VerifySetupTokenRequest { - pub token: String, -} - #[derive(Serialize)] pub(crate) struct SetupStatus { is_setup_complete: bool, @@ -88,54 +83,10 @@ pub(crate) async fn setup_status_handler(State(state): State) -> Json< Json(SetupStatus { is_setup_complete }) } -pub(crate) async fn check_token_handler(State(state): State) -> Json { - let needs_setup = { - let conn = state.db.lock().await; - db::is_setup_complete(&conn).map(|c| !c).unwrap_or(true) - }; - Json(serde_json::json!({ "token_required": needs_setup })) -} - -pub(crate) async fn verify_setup_token_handler( - State(state): State, - Json(body): Json, -) -> Response { - if body.token == *state.setup_token { - use sha2::{Digest, Sha256}; - let mut hasher = Sha256::new(); - hasher.update(state.setup_token.as_bytes()); - let hash_hex = hasher - .finalize() - .iter() - .map(|b| format!("{:02x}", b)) - .collect::(); - - let cookie_val = format!( - "setup_authorized={}; Path=/; HttpOnly; SameSite=Lax; Max-Age=3600", - hash_hex - ); - - Response::builder() - .header(axum::http::header::SET_COOKIE, cookie_val) - .body(axum::body::Body::from( - serde_json::json!({ "success": true }).to_string(), - )) - .unwrap() - } else { - Response::builder() - .status(StatusCode::BAD_REQUEST) - .body(axum::body::Body::from( - serde_json::json!({ "success": false, "error": "Invalid setup token" }).to_string(), - )) - .unwrap() - } -} - // --- JSON API Handlers --- pub async fn api_password_handler( State(state): State, - headers: axum::http::HeaderMap, Json(body): Json, ) -> Response { let needs_setup = { @@ -143,47 +94,16 @@ pub async fn api_password_handler( db::is_setup_complete(&conn).map(|c| !c).unwrap_or(true) }; - if needs_setup { - let is_authorized = headers - .get(axum::http::header::COOKIE) - .and_then(|c| c.to_str().ok()) - .and_then(|c_str| { - c_str.split(';').find_map(|cookie| { - let parts: Vec<&str> = cookie.trim().split('=').collect(); - if parts.len() == 2 && parts[0] == "setup_authorized" { - Some(parts[1]) - } else { - None - } - }) - }) - .map(|cookie_val| { - use sha2::{Digest, Sha256}; - let mut hasher = Sha256::new(); - hasher.update(state.setup_token.as_bytes()); - let expected_hash = hasher - .finalize() - .iter() - .map(|b| format!("{:02x}", b)) - .collect::(); - cookie_val == expected_hash - }) - .unwrap_or(false); - - if !is_authorized { - tracing::warn!( - handler = "api_password_handler", - "unauthorized setup attempt" - ); - return ( - StatusCode::UNAUTHORIZED, - Json(serde_json::json!({ - "success": false, - "error": "Unauthorized: Setup token verification required" - })), - ) - .into_response(); - } + if !needs_setup { + tracing::warn!(handler = "api_password_handler", "setup already complete"); + return ( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ + "success": false, + "error": "Setup is already completed" + })), + ) + .into_response(); } if body.password.len() < 8 { @@ -389,6 +309,14 @@ pub async fn api_finish_handler( ); let _ = db::wal_checkpoint(&conn); drop(conn); + + if flow.relay_opt_in { + let ts = state.tunnel_state.clone(); + tokio::spawn(async move { + let _ = crate::tunnel::TunnelState::start(ts).await; + }); + } + tracing::info!(handler = "api_finish_handler", "setup complete"); Json(serde_json::json!({"success": true})).into_response() } diff --git a/backend/tests/common/mod.rs b/backend/tests/common/mod.rs index 411b02b..73b332a 100644 --- a/backend/tests/common/mod.rs +++ b/backend/tests/common/mod.rs @@ -90,7 +90,6 @@ fn test_app_inner( terminal_state: Arc::new(TerminalState::default()), tunnel_state: Arc::new(tunnel_state), port: 3939, - setup_token: Arc::new("test-setup-token-123".to_string()), }; let router = sysdeck_agent::build_router(app_state.clone()); diff --git a/backend/tests/integration.rs b/backend/tests/integration.rs index badfa3c..a12f5dc 100644 --- a/backend/tests/integration.rs +++ b/backend/tests/integration.rs @@ -14,27 +14,10 @@ use tower::ServiceExt; async fn test_setup_json_api_full_flow() { let (mut router, _state) = test_app(); - // Verify setup token to get setup_authorized cookie - let resp = post_json( - &mut router, - "/api/setup/verify-setup-token", - json!({"token": "test-setup-token-123"}), - ) - .await; - assert_eq!(resp.status(), StatusCode::OK); - let cookie = resp - .headers() - .get(axum::http::header::SET_COOKIE) - .unwrap() - .to_str() - .unwrap() - .to_string(); - // Step 1: Set password - let resp = authed_post_json( + let resp = post_json( &mut router, "/api/setup/password", - &cookie, json!({"password": "MySecureP@ss1"}), ) .await; @@ -101,26 +84,9 @@ async fn test_setup_invalid_token_returns_error() { #[tokio::test] async fn test_setup_weak_password_rejected() { let (mut router, _state) = test_app(); - // Verify setup token to get setup_authorized cookie let resp = post_json( - &mut router, - "/api/setup/verify-setup-token", - json!({"token": "test-setup-token-123"}), - ) - .await; - assert_eq!(resp.status(), StatusCode::OK); - let cookie = resp - .headers() - .get(axum::http::header::SET_COOKIE) - .unwrap() - .to_str() - .unwrap() - .to_string(); - - let resp = authed_post_json( &mut router, "/api/setup/password", - &cookie, json!({"password": "weak"}), ) .await; diff --git a/frontend/src/pages/Setup.tsx b/frontend/src/pages/Setup.tsx index 67e93fb..38d46dd 100644 --- a/frontend/src/pages/Setup.tsx +++ b/frontend/src/pages/Setup.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useRef, useState } from "react" import { useNavigate } from "react-router-dom" -import { Check, Copy, Download, Eye, EyeOff, ShieldAlert } from "lucide-react" +import { Check, Copy, Download, Eye, EyeOff } from "lucide-react" import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Input } from "@/components/ui/input" @@ -347,6 +347,11 @@ function StepRelay({ onComplete }: { onComplete: (enabled: boolean) => void }) { Optionally expose this agent to the internet via a Cloudflare Quick Tunnel. This allows remote access from outside your local network without port forwarding.

+
+ + *Note: Because this uses a free, zero-signup tunnel, your remote URL will change every time the app restarts. We are working on adding persistent domain support soon! + +
void }) { ) } -function StepToken({ onComplete }: { onComplete: () => void }) { - const [inputToken, setInputToken] = useState("") - const [error, setError] = useState("") - const [loading, setLoading] = useState(false) +function StepWelcome({ onComplete }: { onComplete: () => void }) { + return ( +
+
+
+
+ 1 +
+
+

Secure your account

+

+ You will create an admin password and set up Two-Factor Authentication (2FA) to keep your system safe. +

+
+
- async function handleSubmit(e: React.FormEvent) { - e.preventDefault() - setError("") - setLoading(true) - try { - const res = await fetch("/api/setup/verify-setup-token", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ token: inputToken }), - }) - const data = await res.json() - if (data.success) { - onComplete() - } else { - setError("Invalid setup token. Check the server console for the token.") - } - } catch { - setError("Connection error") - } finally { - setLoading(false) - } - } +
+
+ 2 +
+
+

Enable Remote Access (Optional)

+

+ You can turn on the Cloudflare Relay to access this PC from anywhere in the world. +
+ + *Note: Because this uses a free, zero-signup tunnel, your remote URL will change every time the app restarts. We are working on adding persistent domain support soon! + +

+
+
- return ( -
-
- - This server requires a setup token. Enter the token printed in the server console. -
-
- - setInputToken(e.target.value)} - placeholder="Enter setup token" - required - autoComplete="off" - /> +
+
+ 3 +
+
+

Access your dashboard

+

+ Once setup is complete, you'll be able to manage your files, terminal, and system hardware directly from your browser. +

+
+
- {error &&

{error}

} - -
+
) } @@ -425,10 +428,6 @@ export function SetupPage() { const [step, setStep] = useState(0) const [token, setToken] = useState(null) const [checking, setChecking] = useState(true) - const [tokenRequired, setTokenRequired] = useState(false) - const [offset, setOffset] = useState(0) - - const steps = tokenRequired ? ["Token", ...BASE_STEPS] : BASE_STEPS useEffect(() => { async function checkStatus() { @@ -439,19 +438,13 @@ export function SetupPage() { navigate("/login", { replace: true }) return } - const tokenRes = await fetch("/api/setup/check-token") - const tokenData = await tokenRes.json() - if (tokenData.token_required) { - setTokenRequired(true) - setOffset(1) - } const savedToken = sessionStorage.getItem("setup_token") if (savedToken) { const progressRes = await fetch(`/api/setup/progress?token=${savedToken}`) const progressData = await progressRes.json() if (progressData.success) { setToken(savedToken) - setStep(progressData.current_step - 1 + (tokenData.token_required ? 1 : 0)) + setStep(progressData.current_step) } } } catch { @@ -463,23 +456,19 @@ export function SetupPage() { checkStatus() }, [navigate]) - const handleTokenComplete = useCallback(() => { - setStep(1) - }, []) - const handlePasswordComplete = useCallback((newToken: string) => { setToken(newToken) - setStep(1 + offset) - }, [offset]) + setStep(2) + }, []) const handleTotpComplete = useCallback((newToken: string) => { setToken(newToken) - setStep(2 + offset) - }, [offset]) + setStep(3) + }, []) const handleRecoveryComplete = useCallback(() => { - setStep(3 + offset) - }, [offset]) + setStep(4) + }, []) const handleRelayComplete = useCallback(async (enabled: boolean) => { const token = sessionStorage.getItem("setup_token") @@ -511,8 +500,6 @@ export function SetupPage() { ) } - const baseStep = step - offset - return (
@@ -520,28 +507,30 @@ export function SetupPage() {

SysDeck Agent Setup

Configure your agent

- + {step > 0 && } - {steps[step]} + + {step === 0 ? "👋 Welcome to SysDeck" : BASE_STEPS[step - 1]} + - {steps[step] === "Token" ? "Enter the setup token from the server console" : - steps[step] === "Password" ? "Create a strong password to secure your agent" : - steps[step] === "Two-Factor Auth" ? "Set up two-factor authentication" : - steps[step] === "Recovery Codes" ? "Store your recovery codes safely" : + {step === 0 ? "Let's get your remote access set up in 3 quick steps." : + step === 1 ? "Create a strong password to secure your agent" : + step === 2 ? "Set up two-factor authentication" : + step === 3 ? "Store your recovery codes safely" : "Configure remote access"} - {step === 0 && tokenRequired ? ( - - ) : baseStep === 0 ? ( + {step === 0 ? ( + setStep(1)} /> + ) : step === 1 ? ( - ) : baseStep === 1 && token ? ( + ) : step === 2 && token ? ( - ) : baseStep === 2 ? ( + ) : step === 3 ? ( - ) : baseStep === 3 ? ( + ) : step === 4 ? ( ) : null} From 36a7fb2c13b645d9099d68e70b74ce873efde946 Mon Sep 17 00:00:00 2001 From: Aditya Date: Sat, 4 Jul 2026 11:29:06 +0530 Subject: [PATCH 5/6] chore: update changelog for version 1.1.0 release date --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 113b4ca..666ecaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.1.0] - Unreleased +## [1.1.0] - 2026-07-04 ### Added - **Zero-Friction Portable Workflow**: Redesigned the onboarding process by completely removing the setup key requirements. Added a welcoming onboarding screen (Step 0) in the browser before configuring the system. From a12fcf14700e95fe6019b71869ecef0cbd60577d Mon Sep 17 00:00:00 2001 From: Aditya Date: Sat, 4 Jul 2026 11:36:32 +0530 Subject: [PATCH 6/6] fix(clippy): resolve unused-mut warnings in new_command and new_tokio_command on non-Windows platforms --- backend/src/lib.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/backend/src/lib.rs b/backend/src/lib.rs index 0c42d05..b884cdb 100644 --- a/backend/src/lib.rs +++ b/backend/src/lib.rs @@ -79,22 +79,30 @@ pub struct AppState { } pub fn new_command>(program: S) -> std::process::Command { - let mut cmd = std::process::Command::new(program); #[cfg(target_os = "windows")] { use std::os::windows::process::CommandExt; + let mut cmd = std::process::Command::new(program); cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW + cmd + } + #[cfg(not(target_os = "windows"))] + { + std::process::Command::new(program) } - cmd } pub fn new_tokio_command>(program: S) -> tokio::process::Command { - let mut cmd = tokio::process::Command::new(program); #[cfg(target_os = "windows")] { + let mut cmd = tokio::process::Command::new(program); cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW + cmd + } + #[cfg(not(target_os = "windows"))] + { + tokio::process::Command::new(program) } - cmd } pub fn get_data_dir() -> PathBuf {