github.com/jingweno/gh@v2.1.1-0.20221007190738-04a7985fa9a1+incompatible/github/configs.go (about)

     1  package github
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"github.com/howeyc/gopass"
     7  	"github.com/jingweno/gh/utils"
     8  	"io"
     9  	"io/ioutil"
    10  	"os"
    11  	"path/filepath"
    12  	"strconv"
    13  )
    14  
    15  var (
    16  	defaultConfigsFile = filepath.Join(os.Getenv("HOME"), ".config", "gh")
    17  )
    18  
    19  type Credentials struct {
    20  	Host        string `json:"host"`
    21  	User        string `json:"user"`
    22  	AccessToken string `json:"access_token"`
    23  }
    24  
    25  type Configs struct {
    26  	Credentials []Credentials `json:"credentials"`
    27  }
    28  
    29  func (c *Configs) PromptFor(host string) *Credentials {
    30  	cc := c.find(host)
    31  	if cc == nil {
    32  		user := c.PromptForUser()
    33  		pass := c.PromptForPassword(host, user)
    34  
    35  		// Create Client with a stub Credentials
    36  		client := Client{Credentials: &Credentials{Host: host}}
    37  		token, err := client.FindOrCreateToken(user, pass, "")
    38  		if err != nil {
    39  			if ce, ok := err.(*ClientError); ok && ce.Is2FAError() {
    40  				code := c.PromptForOTP()
    41  				token, err = client.FindOrCreateToken(user, pass, code)
    42  			}
    43  		}
    44  		utils.Check(err)
    45  
    46  		cc = &Credentials{Host: host, User: user, AccessToken: token}
    47  		c.Credentials = append(c.Credentials, *cc)
    48  		err = saveTo(configsFile(), c)
    49  		utils.Check(err)
    50  	}
    51  
    52  	return cc
    53  }
    54  
    55  func (c *Configs) PromptForUser() (user string) {
    56  	user = os.Getenv("GITHUB_USER")
    57  	if user != "" {
    58  		return
    59  	}
    60  
    61  	fmt.Printf("%s username: ", GitHubHost)
    62  	fmt.Scanln(&user)
    63  
    64  	return
    65  }
    66  
    67  func (c *Configs) PromptForPassword(host, user string) (pass string) {
    68  	pass = os.Getenv("GITHUB_PASSWORD")
    69  	if pass != "" {
    70  		return
    71  	}
    72  
    73  	fmt.Printf("%s password for %s (never stored): ", host, user)
    74  	if isTerminal(os.Stdout.Fd()) {
    75  		pass = string(gopass.GetPasswd())
    76  	} else {
    77  		fmt.Scanln(&pass)
    78  	}
    79  
    80  	return
    81  }
    82  
    83  func (c *Configs) PromptForOTP() string {
    84  	var code string
    85  	fmt.Print("two-factor authentication code: ")
    86  	fmt.Scanln(&code)
    87  
    88  	return code
    89  }
    90  
    91  func (c *Configs) find(host string) *Credentials {
    92  	for _, t := range c.Credentials {
    93  		if t.Host == host {
    94  			return &t
    95  		}
    96  	}
    97  
    98  	return nil
    99  }
   100  
   101  func saveTo(filename string, v interface{}) error {
   102  	err := os.MkdirAll(filepath.Dir(filename), 0771)
   103  	if err != nil {
   104  		return err
   105  	}
   106  
   107  	f, err := os.Create(filename)
   108  	if err != nil {
   109  		return err
   110  	}
   111  	defer f.Close()
   112  
   113  	enc := json.NewEncoder(f)
   114  	return enc.Encode(v)
   115  }
   116  
   117  func loadFrom(filename string, c *Configs) error {
   118  	return loadFromFile(filename, c)
   119  }
   120  
   121  // Function to load deprecated configuration.
   122  // It's not intended to be used.
   123  func loadFromDeprecated(filename string, c *[]Credentials) error {
   124  	return loadFromFile(filename, c)
   125  }
   126  
   127  func loadFromFile(filename string, v interface{}) error {
   128  	f, err := os.Open(filename)
   129  	if err != nil {
   130  		return err
   131  	}
   132  	defer f.Close()
   133  
   134  	dec := json.NewDecoder(f)
   135  	for {
   136  		if err := dec.Decode(v); err == io.EOF {
   137  			break
   138  		} else if err != nil {
   139  			return err
   140  		}
   141  	}
   142  
   143  	return nil
   144  }
   145  
   146  func configsFile() string {
   147  	configsFile := os.Getenv("GH_CONFIG")
   148  	if configsFile == "" {
   149  		configsFile = defaultConfigsFile
   150  	}
   151  
   152  	return configsFile
   153  }
   154  
   155  func CurrentConfigs() *Configs {
   156  	c := &Configs{}
   157  
   158  	configFile := configsFile()
   159  	err := loadFrom(configFile, c)
   160  
   161  	if err != nil {
   162  		// Try deprecated configuration
   163  		var creds []Credentials
   164  		err := loadFromDeprecated(configsFile(), &creds)
   165  		if err != nil {
   166  			creds = make([]Credentials, 0)
   167  		}
   168  		c.Credentials = creds
   169  		saveTo(configFile, c)
   170  	}
   171  
   172  	return c
   173  }
   174  
   175  func (c *Configs) DefaultCredentials() (credentials *Credentials) {
   176  	if GitHubHostEnv != "" {
   177  		credentials = c.PromptFor(GitHubHostEnv)
   178  	} else if len(c.Credentials) > 0 {
   179  		credentials = c.selectCredentials()
   180  	} else {
   181  		credentials = c.PromptFor(DefaultHost())
   182  	}
   183  
   184  	return
   185  }
   186  
   187  func (c *Configs) selectCredentials() *Credentials {
   188  	options := len(c.Credentials)
   189  
   190  	if options == 1 {
   191  		return &c.Credentials[0]
   192  	}
   193  
   194  	prompt := "Select host:\n"
   195  	for idx, creds := range c.Credentials {
   196  		prompt += fmt.Sprintf(" %d. %s\n", idx+1, creds.Host)
   197  	}
   198  	prompt += fmt.Sprint("> ")
   199  
   200  	fmt.Printf(prompt)
   201  	var index string
   202  	fmt.Scanln(&index)
   203  
   204  	i, err := strconv.Atoi(index)
   205  	if err != nil || i < 1 || i > options {
   206  		utils.Check(fmt.Errorf("Error: must enter a number [1-%d]", options))
   207  	}
   208  
   209  	return &c.Credentials[i-1]
   210  }
   211  
   212  func (c *Configs) Save() error {
   213  	return saveTo(configsFile(), c)
   214  }
   215  
   216  // Public for testing purpose
   217  func CreateTestConfigs(user, token string) *Configs {
   218  	f, _ := ioutil.TempFile("", "test-config")
   219  	defaultConfigsFile = f.Name()
   220  
   221  	creds := []Credentials{
   222  		{User: "jingweno", AccessToken: "123", Host: GitHubHost},
   223  	}
   224  
   225  	c := &Configs{Credentials: creds}
   226  	saveTo(f.Name(), c)
   227  
   228  	return c
   229  }