mirror of
https://gitea.com/gitea/tea.git
synced 2026-03-13 09:13:30 +01:00
feat: store OAuth tokens in OS keyring via credstore (#926)
## Summary - Introduce `github.com/go-authgate/sdk-go/credstore` to store OAuth tokens securely in the OS keyring (macOS Keychain / Linux Secret Service / Windows Credential Manager), with automatic fallback to an encrypted JSON file - Add `AuthMethod` field to `Login` struct; new OAuth logins are marked `auth_method: oauth` and no longer write `token`/`refresh_token`/`token_expiry` to `config.yml` - Add `GetAccessToken()` / `GetRefreshToken()` / `GetTokenExpiry()` accessors that transparently read from credstore for OAuth logins, with fallback to YAML fields for legacy logins - Update all token reference sites across the codebase to use the new accessors - Non-OAuth logins (token, SSH) are completely unaffected; no migration of existing tokens ## Key files | File | Role | |------|------| | `modules/config/credstore.go` | **New** — credstore wrapper (Load/Save/Delete) | | `modules/config/login.go` | Login struct, token accessors, refresh logic | | `modules/auth/oauth.go` | OAuth flow, token creation / re-authentication | | `modules/api/client.go`, `cmd/login/helper.go`, `cmd/login/oauth_refresh.go` | Token reference updates | | `modules/task/pull_*.go`, `modules/task/repo_clone.go` | Git operation token reference updates | ## Test plan - [x] `go build ./...` compiles successfully - [x] `go test ./...` all tests pass - [x] `tea login add --oauth` completes OAuth flow; verify config.yml has `auth_method: oauth` but no token/refresh_token/token_expiry - [x] `tea repos ls` API calls work (token read from credstore) - [x] `tea login delete <name>` credstore token is also removed - [x] Existing non-OAuth logins continue to work unchanged 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed-on: https://gitea.com/gitea/tea/pulls/926 Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com> Co-committed-by: Bo-Yi Wu <appleboy.tw@gmail.com>
This commit is contained in:
@@ -112,7 +112,7 @@ var CmdLoginHelper = cli.Command{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(userConfig.Token) == 0 {
|
if len(userConfig.GetAccessToken()) == 0 {
|
||||||
log.Fatal("User not set")
|
log.Fatal("User not set")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +126,7 @@ var CmdLoginHelper = cli.Command{
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = fmt.Fprintf(os.Stdout, "protocol=%s\nhost=%s\nusername=%s\npassword=%s\n", host.Scheme, host.Host, userConfig.User, userConfig.Token)
|
_, err = fmt.Fprintf(os.Stdout, "protocol=%s\nhost=%s\nusername=%s\npassword=%s\n", host.Scheme, host.Host, userConfig.User, userConfig.GetAccessToken())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ func runLoginOAuthRefresh(_ context.Context, cmd *cli.Command) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if the login has a refresh token
|
// Check if the login has a refresh token
|
||||||
if login.RefreshToken == "" {
|
if login.GetRefreshToken() == "" {
|
||||||
return fmt.Errorf("login '%s' does not have a refresh token. It may have been created using a different authentication method", loginName)
|
return fmt.Errorf("login '%s' does not have a refresh token. It may have been created using a different authentication method", loginName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
5
go.mod
5
go.mod
@@ -12,6 +12,7 @@ require (
|
|||||||
github.com/adrg/xdg v0.5.3
|
github.com/adrg/xdg v0.5.3
|
||||||
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
|
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
|
||||||
github.com/enescakir/emoji v1.0.0
|
github.com/enescakir/emoji v1.0.0
|
||||||
|
github.com/go-authgate/sdk-go v0.2.0
|
||||||
github.com/go-git/go-git/v5 v5.17.0
|
github.com/go-git/go-git/v5 v5.17.0
|
||||||
github.com/muesli/termenv v0.16.0
|
github.com/muesli/termenv v0.16.0
|
||||||
github.com/olekukonko/tablewriter v1.1.3
|
github.com/olekukonko/tablewriter v1.1.3
|
||||||
@@ -27,6 +28,7 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
al.essio.dev/pkg/shellescape v1.6.0 // indirect
|
||||||
charm.land/bubbles/v2 v2.0.0 // indirect
|
charm.land/bubbles/v2 v2.0.0 // indirect
|
||||||
charm.land/bubbletea/v2 v2.0.2 // indirect
|
charm.land/bubbletea/v2 v2.0.2 // indirect
|
||||||
dario.cat/mergo v1.0.2 // indirect
|
dario.cat/mergo v1.0.2 // indirect
|
||||||
@@ -52,6 +54,7 @@ require (
|
|||||||
github.com/cloudflare/circl v1.6.3 // indirect
|
github.com/cloudflare/circl v1.6.3 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||||
|
github.com/danieljoos/wincred v1.2.3 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/davidmz/go-pageant v1.0.2 // indirect
|
github.com/davidmz/go-pageant v1.0.2 // indirect
|
||||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||||
@@ -61,6 +64,7 @@ require (
|
|||||||
github.com/go-fed/httpsig v1.1.0 // indirect
|
github.com/go-fed/httpsig v1.1.0 // indirect
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||||
github.com/go-git/go-billy/v5 v5.8.0 // indirect
|
github.com/go-git/go-billy/v5 v5.8.0 // indirect
|
||||||
|
github.com/godbus/dbus/v5 v5.2.2 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||||
github.com/gorilla/css v1.0.1 // indirect
|
github.com/gorilla/css v1.0.1 // indirect
|
||||||
github.com/hashicorp/go-version v1.8.0 // indirect
|
github.com/hashicorp/go-version v1.8.0 // indirect
|
||||||
@@ -87,6 +91,7 @@ require (
|
|||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
github.com/yuin/goldmark v1.7.16 // indirect
|
github.com/yuin/goldmark v1.7.16 // indirect
|
||||||
github.com/yuin/goldmark-emoji v1.0.6 // indirect
|
github.com/yuin/goldmark-emoji v1.0.6 // indirect
|
||||||
|
github.com/zalando/go-keyring v0.2.6 // indirect
|
||||||
golang.org/x/net v0.49.0 // indirect
|
golang.org/x/net v0.49.0 // indirect
|
||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/text v0.34.0 // indirect
|
golang.org/x/text v0.34.0 // indirect
|
||||||
|
|||||||
14
go.sum
14
go.sum
@@ -1,3 +1,5 @@
|
|||||||
|
al.essio.dev/pkg/shellescape v1.6.0 h1:NxFcEqzFSEVCGN2yq7Huv/9hyCEGVa/TncnOOBBeXHA=
|
||||||
|
al.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
|
||||||
charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=
|
charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=
|
||||||
charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI=
|
charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI=
|
||||||
charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0=
|
charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0=
|
||||||
@@ -87,6 +89,8 @@ github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
|||||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||||
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
|
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
|
||||||
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
|
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
|
||||||
|
github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ=
|
||||||
|
github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@@ -106,6 +110,8 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
|||||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||||
|
github.com/go-authgate/sdk-go v0.2.0 h1:w22f+sAg/YMqnLOcS/4SAuMZXTbPurzkSQBsjb1hcbw=
|
||||||
|
github.com/go-authgate/sdk-go v0.2.0/go.mod h1:RGqvrFdrPnOumndoQQV8qzu8zP1KFUZPdhX0IkWduho=
|
||||||
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
|
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
|
||||||
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
|
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||||
@@ -116,10 +122,14 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMj
|
|||||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||||
github.com/go-git/go-git/v5 v5.17.0 h1:AbyI4xf+7DsjINHMu35quAh4wJygKBKBuXVjV/pxesM=
|
github.com/go-git/go-git/v5 v5.17.0 h1:AbyI4xf+7DsjINHMu35quAh4wJygKBKBuXVjV/pxesM=
|
||||||
github.com/go-git/go-git/v5 v5.17.0/go.mod h1:f82C4YiLx+Lhi8eHxltLeGC5uBTXSFa6PC5WW9o4SjI=
|
github.com/go-git/go-git/v5 v5.17.0/go.mod h1:f82C4YiLx+Lhi8eHxltLeGC5uBTXSFa6PC5WW9o4SjI=
|
||||||
|
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
||||||
|
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||||
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||||
github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=
|
github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=
|
||||||
@@ -190,6 +200,8 @@ github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqR
|
|||||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
|
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
|
||||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
@@ -208,6 +220,8 @@ github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
|
|||||||
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||||
github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
|
github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
|
||||||
github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=
|
github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=
|
||||||
|
github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s=
|
||||||
|
github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ func NewClient(login *config.Login) *Client {
|
|||||||
|
|
||||||
return &Client{
|
return &Client{
|
||||||
baseURL: strings.TrimSuffix(login.URL, "/"),
|
baseURL: strings.TrimSuffix(login.URL, "/"),
|
||||||
token: login.Token,
|
token: login.GetAccessToken(),
|
||||||
httpClient: httpClient,
|
httpClient: httpClient,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -377,22 +377,17 @@ func createLoginFromToken(name, serverURL string, token *oauth2.Token, insecure
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create login object
|
// Create login object with OAuth auth method
|
||||||
login := config.Login{
|
login := config.Login{
|
||||||
Name: name,
|
Name: name,
|
||||||
URL: serverURL,
|
URL: serverURL,
|
||||||
Token: token.AccessToken,
|
Token: token.AccessToken, // temporarily set for Client() validation
|
||||||
RefreshToken: token.RefreshToken,
|
AuthMethod: config.AuthMethodOAuth,
|
||||||
Insecure: insecure,
|
Insecure: insecure,
|
||||||
VersionCheck: true,
|
VersionCheck: true,
|
||||||
Created: time.Now().Unix(),
|
Created: time.Now().Unix(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set token expiry if available
|
|
||||||
if !token.Expiry.IsZero() {
|
|
||||||
login.TokenExpiry = token.Expiry.Unix()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate token by getting user info
|
// Validate token by getting user info
|
||||||
client := login.Client()
|
client := login.Client()
|
||||||
u, _, err := client.GetMyUserInfo()
|
u, _, err := client.GetMyUserInfo()
|
||||||
@@ -400,6 +395,9 @@ func createLoginFromToken(name, serverURL string, token *oauth2.Token, insecure
|
|||||||
return fmt.Errorf("failed to validate token: %s", err)
|
return fmt.Errorf("failed to validate token: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear token from YAML fields (will be stored in credstore)
|
||||||
|
login.Token = ""
|
||||||
|
|
||||||
// Set user info
|
// Set user info
|
||||||
login.User = u.UserName
|
login.User = u.UserName
|
||||||
|
|
||||||
@@ -415,6 +413,11 @@ func createLoginFromToken(name, serverURL string, token *oauth2.Token, insecure
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save tokens to credstore
|
||||||
|
if err := config.SaveOAuthToken(login.Name, token.AccessToken, token.RefreshToken, token.Expiry); err != nil {
|
||||||
|
return fmt.Errorf("failed to save token to secure store: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Printf("Login as %s on %s successful. Added this login as %s\n", login.User, login.URL, login.Name)
|
fmt.Printf("Login as %s on %s successful. Added this login as %s\n", login.User, login.URL, login.Name)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -443,7 +446,11 @@ func ReauthenticateLogin(login *config.Login) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the existing login with new token data
|
if login.IsOAuth() {
|
||||||
|
return config.SaveOAuthTokenFromOAuth2(login.Name, token, login)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy path for non-OAuth logins
|
||||||
login.Token = token.AccessToken
|
login.Token = token.AccessToken
|
||||||
if token.RefreshToken != "" {
|
if token.RefreshToken != "" {
|
||||||
login.RefreshToken = token.RefreshToken
|
login.RefreshToken = token.RefreshToken
|
||||||
@@ -451,7 +458,5 @@ func ReauthenticateLogin(login *config.Login) error {
|
|||||||
if !token.Expiry.IsZero() {
|
if !token.Expiry.IsZero() {
|
||||||
login.TokenExpiry = token.Expiry.Unix()
|
login.TokenExpiry = token.Expiry.Unix()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save updated login
|
|
||||||
return config.SaveLoginTokens(login)
|
return config.SaveLoginTokens(login)
|
||||||
}
|
}
|
||||||
|
|||||||
65
modules/config/credstore.go
Normal file
65
modules/config/credstore.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/adrg/xdg"
|
||||||
|
"github.com/go-authgate/sdk-go/credstore"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
tokenStore *credstore.SecureStore[credstore.Token]
|
||||||
|
tokenStoreOnce sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
func getTokenStore() *credstore.SecureStore[credstore.Token] {
|
||||||
|
tokenStoreOnce.Do(func() {
|
||||||
|
filePath := filepath.Join(xdg.ConfigHome, "tea", "credentials.json")
|
||||||
|
tokenStore = credstore.DefaultTokenSecureStore("tea-cli", filePath)
|
||||||
|
})
|
||||||
|
return tokenStore
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadOAuthToken loads OAuth tokens from the secure store.
|
||||||
|
func LoadOAuthToken(loginName string) (*credstore.Token, error) {
|
||||||
|
tok, err := getTokenStore().Load(loginName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &tok, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveOAuthToken saves OAuth tokens to the secure store.
|
||||||
|
func SaveOAuthToken(loginName, accessToken, refreshToken string, expiresAt time.Time) error {
|
||||||
|
return getTokenStore().Save(loginName, credstore.Token{
|
||||||
|
AccessToken: accessToken,
|
||||||
|
RefreshToken: refreshToken,
|
||||||
|
ExpiresAt: expiresAt,
|
||||||
|
ClientID: loginName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteOAuthToken removes tokens from the secure store.
|
||||||
|
func DeleteOAuthToken(loginName string) error {
|
||||||
|
return getTokenStore().Delete(loginName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveOAuthTokenFromOAuth2 saves an oauth2.Token to credstore, falling back to
|
||||||
|
// the existing login's values for empty refresh token or zero expiry.
|
||||||
|
func SaveOAuthTokenFromOAuth2(loginName string, token *oauth2.Token, login *Login) error {
|
||||||
|
refreshToken := token.RefreshToken
|
||||||
|
if refreshToken == "" {
|
||||||
|
refreshToken = login.GetRefreshToken()
|
||||||
|
}
|
||||||
|
expiry := token.Expiry
|
||||||
|
if expiry.IsZero() {
|
||||||
|
expiry = login.GetTokenExpiry()
|
||||||
|
}
|
||||||
|
return SaveOAuthToken(loginName, token.AccessToken, refreshToken, expiry)
|
||||||
|
}
|
||||||
@@ -33,11 +33,14 @@ const TokenRefreshThreshold = 5 * time.Minute
|
|||||||
// DefaultClientID is the default OAuth2 client ID included in most Gitea instances
|
// DefaultClientID is the default OAuth2 client ID included in most Gitea instances
|
||||||
const DefaultClientID = "d57cb8c4-630c-4168-8324-ec79935e18d4"
|
const DefaultClientID = "d57cb8c4-630c-4168-8324-ec79935e18d4"
|
||||||
|
|
||||||
|
// AuthMethodOAuth marks a login as using OAuth with secure credential storage.
|
||||||
|
const AuthMethodOAuth = "oauth"
|
||||||
|
|
||||||
// Login represents a login to a gitea server, you even could add multiple logins for one gitea server
|
// Login represents a login to a gitea server, you even could add multiple logins for one gitea server
|
||||||
type Login struct {
|
type Login struct {
|
||||||
Name string `yaml:"name"`
|
Name string `yaml:"name"`
|
||||||
URL string `yaml:"url"`
|
URL string `yaml:"url"`
|
||||||
Token string `yaml:"token"`
|
Token string `yaml:"token,omitempty"`
|
||||||
Default bool `yaml:"default"`
|
Default bool `yaml:"default"`
|
||||||
SSHHost string `yaml:"ssh_host"`
|
SSHHost string `yaml:"ssh_host"`
|
||||||
// optional path to the private key
|
// optional path to the private key
|
||||||
@@ -52,10 +55,66 @@ type Login struct {
|
|||||||
User string `yaml:"user"`
|
User string `yaml:"user"`
|
||||||
// Created is auto created unix timestamp
|
// Created is auto created unix timestamp
|
||||||
Created int64 `yaml:"created"`
|
Created int64 `yaml:"created"`
|
||||||
|
// AuthMethod indicates the authentication method ("oauth" for OAuth with credstore)
|
||||||
|
AuthMethod string `yaml:"auth_method,omitempty"`
|
||||||
// RefreshToken is used to renew the access token when it expires
|
// RefreshToken is used to renew the access token when it expires
|
||||||
RefreshToken string `yaml:"refresh_token"`
|
RefreshToken string `yaml:"refresh_token,omitempty"`
|
||||||
// TokenExpiry is when the token expires (unix timestamp)
|
// TokenExpiry is when the token expires (unix timestamp)
|
||||||
TokenExpiry int64 `yaml:"token_expiry"`
|
TokenExpiry int64 `yaml:"token_expiry,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsOAuth returns true if this login uses OAuth with secure credential storage.
|
||||||
|
func (l *Login) IsOAuth() bool {
|
||||||
|
return l.AuthMethod == AuthMethodOAuth
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadOAuthToken loads the OAuth token from credstore, returning nil if
|
||||||
|
// this is not an OAuth login or if the load fails (caller should fallback).
|
||||||
|
func (l *Login) loadOAuthToken() *OAuthToken {
|
||||||
|
if !l.IsOAuth() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
tok, err := LoadOAuthToken(l.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &OAuthToken{
|
||||||
|
AccessToken: tok.AccessToken,
|
||||||
|
RefreshToken: tok.RefreshToken,
|
||||||
|
ExpiresAt: tok.ExpiresAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OAuthToken holds the token fields loaded from credstore.
|
||||||
|
type OAuthToken struct {
|
||||||
|
AccessToken string
|
||||||
|
RefreshToken string
|
||||||
|
ExpiresAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAccessToken returns the effective access token.
|
||||||
|
// For OAuth logins, reads from credstore. For others, returns l.Token directly.
|
||||||
|
func (l *Login) GetAccessToken() string {
|
||||||
|
if tok := l.loadOAuthToken(); tok != nil {
|
||||||
|
return tok.AccessToken
|
||||||
|
}
|
||||||
|
return l.Token
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRefreshToken returns the refresh token.
|
||||||
|
func (l *Login) GetRefreshToken() string {
|
||||||
|
if tok := l.loadOAuthToken(); tok != nil {
|
||||||
|
return tok.RefreshToken
|
||||||
|
}
|
||||||
|
return l.RefreshToken
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTokenExpiry returns the token expiry time.
|
||||||
|
func (l *Login) GetTokenExpiry() time.Time {
|
||||||
|
if tok := l.loadOAuthToken(); tok != nil {
|
||||||
|
return tok.ExpiresAt
|
||||||
|
}
|
||||||
|
return time.Unix(l.TokenExpiry, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLogins return all login available by config
|
// GetLogins return all login available by config
|
||||||
@@ -180,8 +239,14 @@ func DeleteLogin(name string) error {
|
|||||||
return fmt.Errorf("can not delete login '%s', does not exist", name)
|
return fmt.Errorf("can not delete login '%s', does not exist", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isOAuth := config.Logins[idx].IsOAuth()
|
||||||
config.Logins = append(config.Logins[:idx], config.Logins[idx+1:]...)
|
config.Logins = append(config.Logins[:idx], config.Logins[idx+1:]...)
|
||||||
|
|
||||||
|
// Clean up credstore tokens for OAuth logins
|
||||||
|
if isOAuth {
|
||||||
|
_ = DeleteOAuthToken(name)
|
||||||
|
}
|
||||||
|
|
||||||
return saveConfigUnsafe()
|
return saveConfigUnsafe()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -207,6 +272,9 @@ func AddLogin(login *Login) error {
|
|||||||
// SaveLoginTokens updates the token fields for an existing login.
|
// SaveLoginTokens updates the token fields for an existing login.
|
||||||
// This is used after browser-based re-authentication to save new tokens.
|
// This is used after browser-based re-authentication to save new tokens.
|
||||||
func SaveLoginTokens(login *Login) error {
|
func SaveLoginTokens(login *Login) error {
|
||||||
|
if login.IsOAuth() {
|
||||||
|
return SaveOAuthToken(login.Name, login.GetAccessToken(), login.GetRefreshToken(), login.GetTokenExpiry())
|
||||||
|
}
|
||||||
return withConfigLock(func() error {
|
return withConfigLock(func() error {
|
||||||
for i, l := range config.Logins {
|
for i, l := range config.Logins {
|
||||||
if strings.EqualFold(l.Name, login.Name) {
|
if strings.EqualFold(l.Name, login.Name) {
|
||||||
@@ -223,11 +291,21 @@ func SaveLoginTokens(login *Login) error {
|
|||||||
// RefreshOAuthTokenIfNeeded refreshes the OAuth token if it's expired or near expiry.
|
// RefreshOAuthTokenIfNeeded refreshes the OAuth token if it's expired or near expiry.
|
||||||
// Returns nil without doing anything if no refresh is needed.
|
// Returns nil without doing anything if no refresh is needed.
|
||||||
func (l *Login) RefreshOAuthTokenIfNeeded() error {
|
func (l *Login) RefreshOAuthTokenIfNeeded() error {
|
||||||
|
// Load once to avoid multiple credstore reads
|
||||||
|
if tok := l.loadOAuthToken(); tok != nil {
|
||||||
|
if tok.RefreshToken == "" || tok.ExpiresAt.IsZero() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if time.Now().Add(TokenRefreshThreshold).After(tok.ExpiresAt) {
|
||||||
|
return l.RefreshOAuthToken()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Non-OAuth path: use YAML fields
|
||||||
if l.RefreshToken == "" || l.TokenExpiry == 0 {
|
if l.RefreshToken == "" || l.TokenExpiry == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
expiryTime := time.Unix(l.TokenExpiry, 0)
|
if time.Now().Add(TokenRefreshThreshold).After(time.Unix(l.TokenExpiry, 0)) {
|
||||||
if time.Now().Add(TokenRefreshThreshold).After(expiryTime) {
|
|
||||||
return l.RefreshOAuthToken()
|
return l.RefreshOAuthToken()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -238,7 +316,7 @@ func (l *Login) RefreshOAuthTokenIfNeeded() error {
|
|||||||
// Uses double-checked locking to avoid unnecessary refresh calls when multiple
|
// Uses double-checked locking to avoid unnecessary refresh calls when multiple
|
||||||
// processes race to refresh the same token.
|
// processes race to refresh the same token.
|
||||||
func (l *Login) RefreshOAuthToken() error {
|
func (l *Login) RefreshOAuthToken() error {
|
||||||
if l.RefreshToken == "" {
|
if l.GetRefreshToken() == "" {
|
||||||
return fmt.Errorf("no refresh token available")
|
return fmt.Errorf("no refresh token available")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,13 +326,17 @@ func (l *Login) RefreshOAuthToken() error {
|
|||||||
for i, login := range config.Logins {
|
for i, login := range config.Logins {
|
||||||
if login.Name == l.Name {
|
if login.Name == l.Name {
|
||||||
// Check if token was refreshed by another process
|
// Check if token was refreshed by another process
|
||||||
if login.TokenExpiry != l.TokenExpiry && login.TokenExpiry > 0 {
|
currentExpiry := login.GetTokenExpiry()
|
||||||
expiryTime := time.Unix(login.TokenExpiry, 0)
|
ourExpiry := l.GetTokenExpiry()
|
||||||
if time.Now().Add(TokenRefreshThreshold).Before(expiryTime) {
|
if currentExpiry != ourExpiry && !currentExpiry.IsZero() {
|
||||||
|
if time.Now().Add(TokenRefreshThreshold).Before(currentExpiry) {
|
||||||
// Token was refreshed by another process, update our copy
|
// Token was refreshed by another process, update our copy
|
||||||
l.Token = login.Token
|
if !login.IsOAuth() {
|
||||||
l.RefreshToken = login.RefreshToken
|
l.Token = login.Token
|
||||||
l.TokenExpiry = login.TokenExpiry
|
l.RefreshToken = login.RefreshToken
|
||||||
|
l.TokenExpiry = login.TokenExpiry
|
||||||
|
}
|
||||||
|
// For OAuth logins, credstore already has the latest tokens
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -265,7 +347,12 @@ func (l *Login) RefreshOAuthToken() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update login with new token information
|
if l.IsOAuth() {
|
||||||
|
// Save tokens to credstore; no YAML changes needed
|
||||||
|
return SaveOAuthTokenFromOAuth2(l.Name, newToken, l)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update login with new token information (legacy path)
|
||||||
l.Token = newToken.AccessToken
|
l.Token = newToken.AccessToken
|
||||||
if newToken.RefreshToken != "" {
|
if newToken.RefreshToken != "" {
|
||||||
l.RefreshToken = newToken.RefreshToken
|
l.RefreshToken = newToken.RefreshToken
|
||||||
@@ -273,8 +360,6 @@ func (l *Login) RefreshOAuthToken() error {
|
|||||||
if !newToken.Expiry.IsZero() {
|
if !newToken.Expiry.IsZero() {
|
||||||
l.TokenExpiry = newToken.Expiry.Unix()
|
l.TokenExpiry = newToken.Expiry.Unix()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update in config slice and save
|
|
||||||
config.Logins[i] = *l
|
config.Logins[i] = *l
|
||||||
return saveConfigUnsafe()
|
return saveConfigUnsafe()
|
||||||
}
|
}
|
||||||
@@ -286,10 +371,22 @@ func (l *Login) RefreshOAuthToken() error {
|
|||||||
|
|
||||||
// doOAuthRefresh performs the actual OAuth token refresh API call.
|
// doOAuthRefresh performs the actual OAuth token refresh API call.
|
||||||
func doOAuthRefresh(l *Login) (*oauth2.Token, error) {
|
func doOAuthRefresh(l *Login) (*oauth2.Token, error) {
|
||||||
|
// Build current token from credstore (single load) or YAML fields
|
||||||
|
var accessToken, refreshToken string
|
||||||
|
var expiry time.Time
|
||||||
|
if tok := l.loadOAuthToken(); tok != nil {
|
||||||
|
accessToken = tok.AccessToken
|
||||||
|
refreshToken = tok.RefreshToken
|
||||||
|
expiry = tok.ExpiresAt
|
||||||
|
} else {
|
||||||
|
accessToken = l.Token
|
||||||
|
refreshToken = l.RefreshToken
|
||||||
|
expiry = time.Unix(l.TokenExpiry, 0)
|
||||||
|
}
|
||||||
currentToken := &oauth2.Token{
|
currentToken := &oauth2.Token{
|
||||||
AccessToken: l.Token,
|
AccessToken: accessToken,
|
||||||
RefreshToken: l.RefreshToken,
|
RefreshToken: refreshToken,
|
||||||
Expiry: time.Unix(l.TokenExpiry, 0),
|
Expiry: expiry,
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
@@ -341,7 +438,7 @@ func (l *Login) Client(options ...gitea.ClientOption) *gitea.Client {
|
|||||||
options = append([]gitea.ClientOption{gitea.SetGiteaVersion("")}, options...)
|
options = append([]gitea.ClientOption{gitea.SetGiteaVersion("")}, options...)
|
||||||
}
|
}
|
||||||
|
|
||||||
options = append(options, gitea.SetToken(l.Token), gitea.SetHTTPClient(httpClient), gitea.SetUserAgent(httputil.UserAgent()))
|
options = append(options, gitea.SetToken(l.GetAccessToken()), gitea.SetHTTPClient(httpClient), gitea.SetUserAgent(httputil.UserAgent()))
|
||||||
if debug.IsDebug() {
|
if debug.IsDebug() {
|
||||||
options = append(options, gitea.SetDebugMode())
|
options = append(options, gitea.SetDebugMode())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ func doPRFetch(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
auth, err := local_git.GetAuthForURL(url, login.Token, login.SSHKey, callback)
|
auth, err := local_git.GetAuthForURL(url, login.GetAccessToken(), login.SSHKey, callback)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ call me again with the --ignore-sha flag`, remoteBranch)
|
|||||||
if urlErr != nil {
|
if urlErr != nil {
|
||||||
return urlErr
|
return urlErr
|
||||||
}
|
}
|
||||||
auth, authErr := local_git.GetAuthForURL(url, login.Token, login.SSHKey, callback)
|
auth, authErr := local_git.GetAuthForURL(url, login.GetAccessToken(), login.SSHKey, callback)
|
||||||
if authErr != nil {
|
if authErr != nil {
|
||||||
return authErr
|
return authErr
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ func CreateAgitFlowPull(ctx *context.TeaContext, remote, head, base, topic strin
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
auth, err := local_git.GetAuthForURL(url, ctx.Login.Token, ctx.Login.SSHKey, callback)
|
auth, err := local_git.GetAuthForURL(url, ctx.Login.GetAccessToken(), ctx.Login.SSHKey, callback)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ func RepoClone(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
auth, err := local_git.GetAuthForURL(originURL, login.Token, login.SSHKey, callback)
|
auth, err := local_git.GetAuthForURL(originURL, login.GetAccessToken(), login.SSHKey, callback)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ import (
|
|||||||
"charm.land/lipgloss/v2/compat"
|
"charm.land/lipgloss/v2/compat"
|
||||||
)
|
)
|
||||||
|
|
||||||
type myTheme struct{}
|
// TeaTheme implements the huh.Theme interface with tea-cli styling.
|
||||||
|
type TeaTheme struct{}
|
||||||
|
|
||||||
func (t myTheme) Theme(isDark bool) *huh.Styles {
|
// Theme implements the huh.Theme interface.
|
||||||
|
func (t TeaTheme) Theme(isDark bool) *huh.Styles {
|
||||||
theme := huh.ThemeCharm(isDark)
|
theme := huh.ThemeCharm(isDark)
|
||||||
|
|
||||||
title := compat.AdaptiveColor{Light: lipgloss.Color("#02BA84"), Dark: lipgloss.Color("#02BF87")}
|
title := compat.AdaptiveColor{Light: lipgloss.Color("#02BA84"), Dark: lipgloss.Color("#02BF87")}
|
||||||
@@ -20,8 +22,8 @@ func (t myTheme) Theme(isDark bool) *huh.Styles {
|
|||||||
return theme
|
return theme
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTheme returns default theme
|
// GetTheme returns the default theme for huh prompts.
|
||||||
func GetTheme() myTheme {
|
func GetTheme() TeaTheme {
|
||||||
var t myTheme
|
var t TeaTheme
|
||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user