From e6cbf3041502a5727dcceb0231a5c44759ab54dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Papp?= Date: Wed, 6 May 2026 15:57:34 +0200 Subject: [PATCH] [client/ui-wails] Surface daemon SessionExpired in the tray Port the Fyne UI's onSessionExpire 1:1 to the Wails tray so an SSO token expiry no longer leaves the user staring at a stale peer list. When applyStatus sees the transition into the daemon's StatusSessionExpired, fire a single OS notification (the lastStatus guard rate-limits it to the transition itself, mirroring the Fyne sendNotification flag) and bring the main window forward on the /login route so the frontend can drive the renewed SSO flow. The Fyne client achieved the same end with a runSelfCommand "login-url" helper; here the window is already in-process so we route to it directly. --- client/ui-wails/tray.go | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/client/ui-wails/tray.go b/client/ui-wails/tray.go index afed50fd7..cf7d99c8d 100644 --- a/client/ui-wails/tray.go +++ b/client/ui-wails/tray.go @@ -56,11 +56,18 @@ const ( notifyErrorTitle = "Error" notifyErrorConnect = "Failed to connect" notifyErrorDisconnect = "Failed to disconnect" + notifySessionExpiredTitle = "NetBird session expired" + notifySessionExpiredBody = "Your NetBird session has expired. Please log in again." // Notification IDs (used to coalesce duplicate toasts). - notifyIDUpdatePrefix = "netbird-update-" - notifyIDEvent = "netbird-event-" - notifyIDTrayError = "netbird-tray-error" + notifyIDUpdatePrefix = "netbird-update-" + notifyIDEvent = "netbird-event-" + notifyIDTrayError = "netbird-tray-error" + notifyIDSessionExpired = "netbird-session-expired" + + // Daemon status string for an SSO session that has expired and needs + // re-authentication. Mirrors internal.StatusSessionExpired. + statusSessionExpired = "SessionExpired" // External URLs. urlGitHubRepo = "https://github.com/netbirdio/netbird" @@ -405,6 +412,13 @@ func (t *Tray) applyStatus(st services.Status) { t.mu.Lock() connected := strings.EqualFold(st.Status, "Connected") iconChanged := connected != t.connected || st.Status != t.lastStatus + // Detect the transition into SessionExpired: the daemon emits the + // state on every Status snapshot for as long as the session stays + // expired, so without this guard we would re-fire the notification + // on every push. Mirrors the legacy Fyne client's sendNotification + // flag in onSessionExpire. + sessionExpiredEnter := strings.EqualFold(st.Status, statusSessionExpired) && + !strings.EqualFold(t.lastStatus, statusSessionExpired) t.connected = connected t.lastStatus = st.Status @@ -428,6 +442,24 @@ func (t *Tray) applyStatus(st services.Status) { if exitNodesChanged { t.rebuildExitNodes(exitNodes) } + if sessionExpiredEnter { + t.handleSessionExpired() + } +} + +// handleSessionExpired surfaces the SSO re-authentication path when the +// daemon reports StatusSessionExpired. Posts a single OS notification +// (the applyStatus guard ensures it fires only on the transition, not +// on every status snapshot) and brings the main window forward so the +// frontend's /login route can drive the renewed SSO flow. Mirrors the +// Fyne client's onSessionExpire, which used a runSelfCommand to spawn +// the login-url helper; here the window is already in-process. +func (t *Tray) handleSessionExpired() { + t.notify(notifySessionExpiredTitle, notifySessionExpiredBody, notifyIDSessionExpired) + if t.window != nil { + t.window.SetURL("/#/login") + t.window.Show() + } } func (t *Tray) rebuildExitNodes(nodes []string) {