github.com/cli/cli@v1.14.1-0.20210902173923-1af6a669e342/pkg/cmd/auth/logout/logout.go (about)

     1  package logout
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"net/http"
     7  
     8  	"github.com/AlecAivazis/survey/v2"
     9  	"github.com/MakeNowJust/heredoc"
    10  	"github.com/cli/cli/api"
    11  	"github.com/cli/cli/internal/config"
    12  	"github.com/cli/cli/pkg/cmdutil"
    13  	"github.com/cli/cli/pkg/iostreams"
    14  	"github.com/cli/cli/pkg/prompt"
    15  	"github.com/spf13/cobra"
    16  )
    17  
    18  type LogoutOptions struct {
    19  	HttpClient func() (*http.Client, error)
    20  	IO         *iostreams.IOStreams
    21  	Config     func() (config.Config, error)
    22  
    23  	Hostname string
    24  }
    25  
    26  func NewCmdLogout(f *cmdutil.Factory, runF func(*LogoutOptions) error) *cobra.Command {
    27  	opts := &LogoutOptions{
    28  		HttpClient: f.HttpClient,
    29  		IO:         f.IOStreams,
    30  		Config:     f.Config,
    31  	}
    32  
    33  	cmd := &cobra.Command{
    34  		Use:   "logout",
    35  		Args:  cobra.ExactArgs(0),
    36  		Short: "Log out of a GitHub host",
    37  		Long: heredoc.Doc(`Remove authentication for a GitHub host.
    38  
    39  			This command removes the authentication configuration for a host either specified
    40  			interactively or via --hostname.
    41  		`),
    42  		Example: heredoc.Doc(`
    43  			$ gh auth logout
    44  			# => select what host to log out of via a prompt
    45  
    46  			$ gh auth logout --hostname enterprise.internal
    47  			# => log out of specified host
    48  		`),
    49  		RunE: func(cmd *cobra.Command, args []string) error {
    50  			if opts.Hostname == "" && !opts.IO.CanPrompt() {
    51  				return &cmdutil.FlagError{Err: errors.New("--hostname required when not running interactively")}
    52  			}
    53  
    54  			if runF != nil {
    55  				return runF(opts)
    56  			}
    57  
    58  			return logoutRun(opts)
    59  		},
    60  	}
    61  
    62  	cmd.Flags().StringVarP(&opts.Hostname, "hostname", "h", "", "The hostname of the GitHub instance to log out of")
    63  
    64  	return cmd
    65  }
    66  
    67  func logoutRun(opts *LogoutOptions) error {
    68  	hostname := opts.Hostname
    69  
    70  	cfg, err := opts.Config()
    71  	if err != nil {
    72  		return err
    73  	}
    74  
    75  	candidates, err := cfg.Hosts()
    76  	if err != nil {
    77  		return err
    78  	}
    79  	if len(candidates) == 0 {
    80  		return fmt.Errorf("not logged in to any hosts")
    81  	}
    82  
    83  	if hostname == "" {
    84  		if len(candidates) == 1 {
    85  			hostname = candidates[0]
    86  		} else {
    87  			err = prompt.SurveyAskOne(&survey.Select{
    88  				Message: "What account do you want to log out of?",
    89  				Options: candidates,
    90  			}, &hostname)
    91  
    92  			if err != nil {
    93  				return fmt.Errorf("could not prompt: %w", err)
    94  			}
    95  		}
    96  	} else {
    97  		var found bool
    98  		for _, c := range candidates {
    99  			if c == hostname {
   100  				found = true
   101  				break
   102  			}
   103  		}
   104  
   105  		if !found {
   106  			return fmt.Errorf("not logged into %s", hostname)
   107  		}
   108  	}
   109  
   110  	if err := cfg.CheckWriteable(hostname, "oauth_token"); err != nil {
   111  		var roErr *config.ReadOnlyEnvError
   112  		if errors.As(err, &roErr) {
   113  			fmt.Fprintf(opts.IO.ErrOut, "The value of the %s environment variable is being used for authentication.\n", roErr.Variable)
   114  			fmt.Fprint(opts.IO.ErrOut, "To erase credentials stored in GitHub CLI, first clear the value from the environment.\n")
   115  			return cmdutil.SilentError
   116  		}
   117  		return err
   118  	}
   119  
   120  	httpClient, err := opts.HttpClient()
   121  	if err != nil {
   122  		return err
   123  	}
   124  	apiClient := api.NewClientFromHTTP(httpClient)
   125  
   126  	username, err := api.CurrentLoginName(apiClient, hostname)
   127  	if err != nil {
   128  		// suppressing; the user is trying to delete this token and it might be bad.
   129  		// we'll see if the username is in the config and fall back to that.
   130  		username, _ = cfg.Get(hostname, "user")
   131  	}
   132  
   133  	usernameStr := ""
   134  	if username != "" {
   135  		usernameStr = fmt.Sprintf(" account '%s'", username)
   136  	}
   137  
   138  	if opts.IO.CanPrompt() {
   139  		var keepGoing bool
   140  		err := prompt.SurveyAskOne(&survey.Confirm{
   141  			Message: fmt.Sprintf("Are you sure you want to log out of %s%s?", hostname, usernameStr),
   142  			Default: true,
   143  		}, &keepGoing)
   144  		if err != nil {
   145  			return fmt.Errorf("could not prompt: %w", err)
   146  		}
   147  
   148  		if !keepGoing {
   149  			return nil
   150  		}
   151  	}
   152  
   153  	cfg.UnsetHost(hostname)
   154  	err = cfg.Write()
   155  	if err != nil {
   156  		return fmt.Errorf("failed to write config, authentication configuration not updated: %w", err)
   157  	}
   158  
   159  	isTTY := opts.IO.IsStdinTTY() && opts.IO.IsStdoutTTY()
   160  
   161  	if isTTY {
   162  		cs := opts.IO.ColorScheme()
   163  		fmt.Fprintf(opts.IO.ErrOut, "%s Logged out of %s%s\n",
   164  			cs.SuccessIcon(), cs.Bold(hostname), usernameStr)
   165  	}
   166  
   167  	return nil
   168  }