diff --git a/client/internal/profilemanager/state.go b/client/internal/profilemanager/state.go index f09391ede..7e7196fdb 100644 --- a/client/internal/profilemanager/state.go +++ b/client/internal/profilemanager/state.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "os" "path/filepath" "github.com/netbirdio/netbird/util" @@ -59,3 +60,22 @@ func (pm *ProfileManager) SetActiveProfileState(state *ProfileState) error { return nil } + +// RemoveProfileState deletes the per-profile state file (which holds the +// account email used for the SSO login hint and the UI display). Called after +// a successful logout so a logged-out profile no longer shows a stale account +// email. The state file only stores the email, so deleting it is equivalent to +// clearing it; the next SSO login recreates it. A missing file is not an error. +func (pm *ProfileManager) RemoveProfileState(profileName string) error { + configDir, err := getConfigDir() + if err != nil { + return fmt.Errorf("get config directory: %w", err) + } + + stateFile := filepath.Join(configDir, profileName+".state.json") + if err := os.Remove(stateFile); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("remove profile state: %w", err) + } + + return nil +} diff --git a/client/server/server.go b/client/server/server.go index 6a7e4b530..685314819 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -1210,7 +1210,19 @@ func (s *Server) sendLogoutRequestWithConfig(ctx context.Context, config *profil } }() - return mgmClient.Logout() + if err := mgmClient.Logout(); err != nil { + // The peer is already gone from the management server (e.g. deleted + // from the dashboard). The logout's goal — deregistering this peer — + // is therefore already satisfied, so treat NotFound as success rather + // than blocking the logout/profile-removal flow. + if logoutPeerGone(err) { + log.Infof("peer already removed from management server, treating logout as successful") + return nil + } + return err + } + + return nil } // Status returns the daemon status @@ -2117,3 +2129,15 @@ func persistLoginOverrides(activeProf *profilemanager.ActiveProfileState, manage } return nil } + +// logoutPeerGone reports whether a management Logout failed because the peer +// no longer exists server-side (gRPC NotFound), walking the wrap chain since +// the client wraps the gRPC status with fmt.Errorf. +func logoutPeerGone(err error) bool { + for e := err; e != nil; e = errors.Unwrap(e) { + if s, ok := gstatus.FromError(e); ok && s.Code() == codes.NotFound { + return true + } + } + return false +} diff --git a/client/ui/services/connection.go b/client/ui/services/connection.go index 95d98aa74..794746989 100644 --- a/client/ui/services/connection.go +++ b/client/ui/services/connection.go @@ -12,8 +12,10 @@ import ( "runtime" "strings" + log "github.com/sirupsen/logrus" gstatus "google.golang.org/grpc/status" + "github.com/netbirdio/netbird/client/internal/profilemanager" "github.com/netbirdio/netbird/client/proto" "github.com/netbirdio/netbird/client/ui/i18n" "github.com/netbirdio/netbird/client/ui/preferences" @@ -339,5 +341,17 @@ func (s *Connection) Logout(ctx context.Context, p LogoutParams) error { if _, err = cli.Logout(ctx, req); err != nil { return s.classifyDaemonError(err) } + + // The daemon runs as root and can't reach the user-owned per-profile state + // file that holds the account email (see Profiles.List). Drop it here from + // the UI process so a logged-out profile no longer shows a stale email; the + // next SSO login recreates it. + if p.ProfileName != "" { + if err := profilemanager.NewProfileManager().RemoveProfileState(p.ProfileName); err != nil { + // Non-fatal: the logout itself succeeded. + log.Warnf("failed to remove profile state for %s: %v", p.ProfileName, err) + } + } + return nil }