From 0986a36aca20c22ec3b79464a122ab0b192de433 Mon Sep 17 00:00:00 2001 From: nessi Date: Tue, 17 Mar 2026 21:34:53 +0100 Subject: [PATCH] feat: add logout functionality and redesign desktop client UI for simplified workflow Add clear_session command to remove session state and profile files from disk. Add resetEnrollment handler in frontend to clear local state and invoke clear_session. Remove hero surface section with profile metadata tiles. Simplify top strip to show profile label in brand copy when enrolled. Add Logout button to top actions and resources sidebar. Redesign status panel with simplified labels and layout. Update surface --- desktop-client/src-tauri/src/lib.rs | 20 ++++++- desktop-client/src/App.tsx | 81 ++++++++++++++--------------- desktop-client/src/styles.css | 55 +++----------------- 3 files changed, 65 insertions(+), 91 deletions(-) diff --git a/desktop-client/src-tauri/src/lib.rs b/desktop-client/src-tauri/src/lib.rs index 469b629..2751410 100644 --- a/desktop-client/src-tauri/src/lib.rs +++ b/desktop-client/src-tauri/src/lib.rs @@ -213,6 +213,24 @@ fn load_state(app: AppHandle, state: State<'_, AppState>) -> Result) -> Result<(), String> { + let app_dir = ensure_app_dir(&app)?; + let session_path = app_dir.join("session.json"); + let profile_path = app_dir.join(format!("{}.conf", PROFILE_NAME)); + + if session_path.exists() { + fs::remove_file(&session_path).map_err(|err| format!("Unable to remove session state: {}", err))?; + } + if profile_path.exists() { + fs::remove_file(&profile_path).map_err(|err| format!("Unable to remove profile: {}", err))?; + } + + let mut session = state.session.lock().map_err(|_| "Unable to clear client state".to_string())?; + *session = None; + Ok(()) +} + #[tauri::command] async fn sync_profile(app: AppHandle, state: State<'_, AppState>) -> Result { let existing = { @@ -417,7 +435,7 @@ pub fn run() { } _ => {} }) - .invoke_handler(tauri::generate_handler![load_state, enroll_device, sync_profile, connect_tunnel, disconnect_tunnel]) + .invoke_handler(tauri::generate_handler![load_state, clear_session, enroll_device, sync_profile, connect_tunnel, disconnect_tunnel]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/desktop-client/src/App.tsx b/desktop-client/src/App.tsx index cdc58b2..61b7f3f 100644 --- a/desktop-client/src/App.tsx +++ b/desktop-client/src/App.tsx @@ -103,6 +103,17 @@ export function App() { } } + async function resetEnrollment() { + try { + await invoke("clear_session"); + setConnected(false); + setState(null); + setError(null); + } catch (err) { + setError(formatInvokeError(err, "Unable to clear local profile")); + } + } + return (
@@ -111,14 +122,19 @@ export function App() { NexaVPN

NexaVPN

-

Desktop Access Client

-

Provisioned WireGuard access with profile sync and one-click reconnect.

+

VPN Client

+

{state ? profileLabel : "Sign in to provision this device"}

{state ? ( + ) : null} + {state ? ( + ) : null}
-
-
-

Managed Tunnel

-

Private access with fewer moving parts.

-

- NexaVPN signs you in, enrolls the device, stores the WireGuard profile locally, and lets you resync the - assigned access profile after policy changes. -

-
-
-
- Current profile - {profileLabel} -
-
- Last sync - {state?.lastSyncTime ?? "Not synced yet"} -
-
-
-
{!state ? (
-

Onboarding

-

Sign in and provision this device

+

Sign in

+

Provision device

@@ -182,11 +177,11 @@ export function App() {
-

Connection

+

Status

{connected ? "Connected" : "Disconnected"}

- {connected ? "Tunnel active" : "Ready to connect"} + {connected ? "Tunnel active" : "Ready"}
@@ -194,8 +189,8 @@ export function App() {
-

Session

-

Provisioned device state

+

Connection

+

Current profile

@@ -208,8 +203,8 @@ export function App() { {state.gatewayEndpoint}
- Profile revision - {state.profileRevision} + Profile + {profileLabel}
Last sync @@ -221,8 +216,8 @@ export function App() {
-

Profile

-

Assigned access profile

+

Local

+

Stored config

@@ -231,8 +226,8 @@ export function App() { {state.profilePath}
- Tunnel strategy - {state.tunnelStrategy} + Revision + {state.profileRevision}
@@ -244,19 +239,21 @@ export function App() {
diff --git a/desktop-client/src/styles.css b/desktop-client/src/styles.css index c8ebc81..81e5cdd 100644 --- a/desktop-client/src/styles.css +++ b/desktop-client/src/styles.css @@ -35,7 +35,7 @@ input { width: min(1120px, 100%); margin: 0 auto; display: grid; - gap: 22px; + gap: 18px; } .top-strip { @@ -72,7 +72,6 @@ input { } .brand-copy h1, -.hero-copy h2, .status-panel h3 { margin: 0; } @@ -120,7 +119,6 @@ input { cursor: default; } -.hero-surface, .surface, .status-panel, .login-panel { @@ -130,50 +128,6 @@ input { backdrop-filter: blur(18px); } -.hero-surface { - border-radius: 30px; - padding: 28px; - display: grid; - grid-template-columns: 1.2fr 0.8fr; - gap: 20px; - align-items: center; -} - -.hero-copy { - display: grid; - gap: 14px; -} - -.hero-copy h2 { - font-size: clamp(2rem, 4vw, 3.6rem); - line-height: 1.04; - max-width: 10ch; -} - -.hero-copy p { - max-width: 56ch; - font-size: 1.03rem; - line-height: 1.6; -} - -.hero-meta { - display: grid; - gap: 14px; -} - -.meta-tile { - padding: 18px; - border-radius: 22px; - background: linear-gradient(180deg, rgba(117, 227, 186, 0.08), rgba(117, 227, 186, 0.02)); - border: 1px solid rgba(117, 227, 186, 0.16); -} - -.meta-tile strong { - display: block; - margin-top: 8px; - font-size: 1.1rem; -} - .body-grid { display: grid; grid-template-columns: 1.3fr 0.7fr; @@ -188,6 +142,12 @@ input { gap: 20px; } +.status-panel > p { + margin: 0; + color: #9eb1d1; + line-height: 1.5; +} + .status-top { display: flex; align-items: center; @@ -310,7 +270,6 @@ input { } @media (max-width: 960px) { - .hero-surface, .body-grid { grid-template-columns: 1fr; }