feat: add profile sync functionality and redesign desktop client UI

Add sync_profile command to fetch latest profile from backend without re-enrollment. Add DeviceView struct to EnrollResponse. Replace hardcoded "just now" timestamp with now_label helper using Unix epoch seconds. Add sync button to UI with loading state. Redesign client interface with top strip containing brand lockup and action buttons, hero surface with profile metadata tiles, body grid with login/status panels and resources sidebar
This commit is contained in:
2026-03-17 21:24:50 +01:00
parent 72c5bb6f55
commit a4c5a3f0ca
4 changed files with 585 additions and 139 deletions

View File

@@ -4,6 +4,12 @@ use std::{
process::{Command, ExitCode},
};
#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;
#[cfg(target_os = "windows")]
const CREATE_NO_WINDOW: u32 = 0x08000000;
fn main() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
@@ -33,14 +39,16 @@ fn run() -> Result<(), String> {
fn connect(profile: &Path) -> Result<(), String> {
#[cfg(target_os = "windows")]
{
ensure_windows_admin()?;
let wireguard = find_windows_wireguard()?;
let status = Command::new(wireguard)
let output = Command::new(wireguard)
.arg("/installtunnelservice")
.arg(profile)
.status()
.creation_flags(CREATE_NO_WINDOW)
.output()
.map_err(|err| format!("unable to launch WireGuard runtime: {err}"))?;
if !status.success() {
return Err(format!("WireGuard runtime connect failed with status {status}"));
if !output.status.success() {
return Err(format_windows_runtime_error("connect", &output));
}
return Ok(());
}
@@ -66,18 +74,20 @@ fn connect(profile: &Path) -> Result<(), String> {
fn disconnect(profile: &Path) -> Result<(), String> {
#[cfg(target_os = "windows")]
{
ensure_windows_admin()?;
let wireguard = find_windows_wireguard()?;
let tunnel_name = profile
.file_stem()
.and_then(|value| value.to_str())
.ok_or_else(|| "invalid profile filename".to_string())?;
let status = Command::new(wireguard)
let output = Command::new(wireguard)
.arg("/uninstalltunnelservice")
.arg(tunnel_name)
.status()
.creation_flags(CREATE_NO_WINDOW)
.output()
.map_err(|err| format!("unable to launch WireGuard runtime: {err}"))?;
if !status.success() {
return Err(format!("WireGuard runtime disconnect failed with status {status}"));
if !output.status.success() {
return Err(format_windows_runtime_error("disconnect", &output));
}
return Ok(());
}
@@ -112,3 +122,35 @@ fn find_windows_wireguard() -> Result<PathBuf, String> {
.find(|path| path.exists())
.ok_or_else(|| "required Windows tunnel runtime is not available".to_string())
}
#[cfg(target_os = "windows")]
fn ensure_windows_admin() -> Result<(), String> {
let status = Command::new("net")
.arg("session")
.creation_flags(CREATE_NO_WINDOW)
.status()
.map_err(|err| format!("unable to determine Windows privilege level: {err}"))?;
if status.success() {
return Ok(());
}
Err("Administrator rights are required to activate the VPN tunnel on Windows. Start NexaVPN as Administrator for now.".into())
}
#[cfg(target_os = "windows")]
fn format_windows_runtime_error(action: &str, output: &std::process::Output) -> String {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
let details = if !stderr.trim().is_empty() {
stderr.trim()
} else {
stdout.trim()
};
if details.is_empty() {
return format!("WireGuard runtime {} failed with status {}", action, output.status);
}
format!("WireGuard runtime {} failed: {}", action, details)
}