diff --git a/client/server/server.go b/client/server/server.go index d84941023..de24d7faa 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -1539,8 +1539,21 @@ func (s *Server) WaitExtendAuthSession( return nil, gstatus.Errorf(codes.InvalidArgument, "invalid device code or no active extend-session flow") } - tokenInfo, err := oAuthFlow.WaitToken(ctx, authInfo) + // Preempt a previous WaitExtendAuthSession (e.g. when the tray + // notification and the about-to-expire dialog both start a flow on + // the same deadline). The older waiter exits via context.Canceled; + // the new one takes over the IdP poll. + s.extendAuthSessionFlow.CancelWait() + + waitCtx, cancel := context.WithCancel(ctx) + defer cancel() + s.extendAuthSessionFlow.SetWaitCancel(cancel) + + tokenInfo, err := oAuthFlow.WaitToken(waitCtx, authInfo) if err != nil { + if errors.Is(err, context.Canceled) { + return nil, gstatus.Errorf(codes.Canceled, "extend-session flow preempted") + } return nil, gstatus.Errorf(codes.Internal, "failed to obtain JWT token: %v", err) } diff --git a/client/ui/authsession/service.go b/client/ui/authsession/service.go index 8cb423da2..5cc96aca4 100644 --- a/client/ui/authsession/service.go +++ b/client/ui/authsession/service.go @@ -6,6 +6,9 @@ import ( "context" "time" + "google.golang.org/grpc/codes" + gstatus "google.golang.org/grpc/status" + "github.com/netbirdio/netbird/client/proto" ) @@ -34,9 +37,12 @@ type ExtendWaitParams struct { // ExtendResult carries the refreshed deadline. ExpiresAt is nil when the // management server reported the peer is not eligible for session -// extension. +// extension. Preempted is true when a newer WaitExtend (e.g. started from +// another UI surface for the same deadline) took over the IdP poll — +// callers should treat the call as a no-op rather than a failure. type ExtendResult struct { ExpiresAt *time.Time `json:"sessionExpiresAt,omitempty"` + Preempted bool `json:"preempted,omitempty"` } // DaemonConn yields a lazy daemon gRPC client. Mirrors services.DaemonConn @@ -101,6 +107,9 @@ func (s *Session) WaitExtend(ctx context.Context, p ExtendWaitParams) (ExtendRes UserCode: p.UserCode, }) if err != nil { + if st, ok := gstatus.FromError(err); ok && st.Code() == codes.Canceled { + return ExtendResult{Preempted: true}, nil + } return ExtendResult{}, err } diff --git a/client/ui/frontend/src/modules/session/SessionAboutToExpireDialog.tsx b/client/ui/frontend/src/modules/session/SessionAboutToExpireDialog.tsx index 1d07b0988..63ba4d2eb 100644 --- a/client/ui/frontend/src/modules/session/SessionAboutToExpireDialog.tsx +++ b/client/ui/frontend/src/modules/session/SessionAboutToExpireDialog.tsx @@ -73,10 +73,20 @@ export default function SessionAboutToExpireDialog() { console.debug("OpenURL failed during extend", e); } } - await Session.WaitExtend({ + const result = await Session.WaitExtend({ deviceCode: start.deviceCode, userCode: start.userCode, }); + if (result.preempted) { + // Another UI surface (e.g. the tray "Extend now" + // notification action) started a flow for the same + // deadline and took over. Keep the dialog open so the + // user can re-trigger if the other flow also fails; + // a successful extend elsewhere refreshes the deadline + // and this window auto-closes when it's no longer + // relevant. + return; + } WindowManager.CloseSessionAboutToExpire().catch(console.error); } catch (e) { await Dialogs.Error({ diff --git a/client/ui/tray.go b/client/ui/tray.go index 5d1114db4..c677052ba 100644 --- a/client/ui/tray.go +++ b/client/ui/tray.go @@ -1391,14 +1391,22 @@ func (t *Tray) runExtendSession() { } } - if _, err := t.svc.Session.WaitExtend(ctx, services.ExtendWaitParams{ + result, err := t.svc.Session.WaitExtend(ctx, services.ExtendWaitParams{ DeviceCode: start.DeviceCode, UserCode: start.UserCode, - }); err != nil { + }) + if err != nil { log.Warnf("session-warning: WaitExtend failed: %v", err) t.notifyError(t.loc.T("notify.sessionWarning.failed")) return } + if result.Preempted { + // Another UI surface (e.g. the about-to-expire dialog) started a + // flow for the same deadline and took over. Stay silent so the + // user only sees the outcome of the surviving flow. + log.Debugf("session-warning: WaitExtend preempted by a newer flow") + return + } t.notify(t.loc.T("notify.sessionWarning.successTitle"), t.loc.T("notify.sessionWarning.successBody"), notifyIDSessionWarning) }