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 }