forked from Gitlink/gitlink-cli
feat(auth): login 新增 --with-token 标准输入非交互登录(对标 gh)
This commit is contained in:
parent
b8e92895f4
commit
6ef51c7d5d
|
|
@ -852,6 +852,7 @@ To reuse the active token in scripts (e.g. raw `curl` calls against endpoints th
|
||||||
```bash
|
```bash
|
||||||
curl -H "Authorization: Bearer $(gitlink-cli auth token)" https://www.gitlink.org.cn/api/v1/...
|
curl -H "Authorization: Bearer $(gitlink-cli auth token)" https://www.gitlink.org.cn/api/v1/...
|
||||||
gitlink-cli auth status --show-token # Inspect the raw token (hidden by default)
|
gitlink-cli auth status --show-token # Inspect the raw token (hidden by default)
|
||||||
|
echo $MY_TOKEN | gitlink-cli auth login --with-token # Non-interactive login (CI, scripts)
|
||||||
```
|
```
|
||||||
|
|
||||||
Priority: `GITLINK_TOKEN` env var > keyring/file stored token. When the env var is not set, the original interactive login flow works as before.
|
Priority: `GITLINK_TOKEN` env var > keyring/file stored token. When the env var is not set, the original interactive login flow works as before.
|
||||||
|
|
|
||||||
|
|
@ -724,6 +724,7 @@ gitlink-cli auth status # 显示 "✓ Logged in via GITLINK_TOKEN environment
|
||||||
```bash
|
```bash
|
||||||
curl -H "Authorization: Bearer $(gitlink-cli auth token)" https://www.gitlink.org.cn/api/v1/...
|
curl -H "Authorization: Bearer $(gitlink-cli auth token)" https://www.gitlink.org.cn/api/v1/...
|
||||||
gitlink-cli auth status --show-token # 查看原始 token(默认隐藏)
|
gitlink-cli auth status --show-token # 查看原始 token(默认隐藏)
|
||||||
|
echo $MY_TOKEN | gitlink-cli auth login --with-token # 非交互登录(CI/脚本)
|
||||||
```
|
```
|
||||||
|
|
||||||
Token 优先级:`GITLINK_TOKEN` 环境变量 > keyring/文件存储的 token。不设置环境变量时完全兼容原有交互式登录。
|
Token 优先级:`GITLINK_TOKEN` 环境变量 > keyring/文件存储的 token。不设置环境变量时完全兼容原有交互式登录。
|
||||||
|
|
|
||||||
|
|
@ -40,11 +40,15 @@ func NewAuthCmd(translators ...*i18n.Translator) *cobra.Command {
|
||||||
|
|
||||||
func newLoginCmd(tr *i18n.Translator) *cobra.Command {
|
func newLoginCmd(tr *i18n.Translator) *cobra.Command {
|
||||||
var tokenMode bool
|
var tokenMode bool
|
||||||
|
var withToken bool
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "login",
|
Use: "login",
|
||||||
Short: tr.T("cmd.auth.login.short"),
|
Short: tr.T("cmd.auth.login.short"),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if withToken {
|
||||||
|
return loginWithTokenStdin(cmd.InOrStdin(), cmd.OutOrStdout(), tr)
|
||||||
|
}
|
||||||
if tokenMode {
|
if tokenMode {
|
||||||
return loginWithToken(cmd.InOrStdin(), cmd.OutOrStdout(), tr)
|
return loginWithToken(cmd.InOrStdin(), cmd.OutOrStdout(), tr)
|
||||||
}
|
}
|
||||||
|
|
@ -52,6 +56,7 @@ func newLoginCmd(tr *i18n.Translator) *cobra.Command {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
cmd.Flags().BoolVar(&tokenMode, "token", false, tr.T("flag.auth.token"))
|
cmd.Flags().BoolVar(&tokenMode, "token", false, tr.T("flag.auth.token"))
|
||||||
|
cmd.Flags().BoolVar(&withToken, "with-token", false, tr.T("flag.auth.with_token"))
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,6 +123,26 @@ func loginWithToken(in io.Reader, out io.Writer, tr *i18n.Translator) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loginWithTokenStdin reads a token from stdin without prompting, mirroring
|
||||||
|
// `gh auth login --with-token` for non-interactive use (CI, scripts):
|
||||||
|
//
|
||||||
|
// echo $TOKEN | gitlink-cli auth login --with-token
|
||||||
|
func loginWithTokenStdin(in io.Reader, out io.Writer, tr *i18n.Translator) error {
|
||||||
|
data, err := io.ReadAll(io.LimitReader(in, 4096))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
token := strings.TrimSpace(string(data))
|
||||||
|
if token == "" {
|
||||||
|
return errors.New(tr.T("error.auth.token_empty"))
|
||||||
|
}
|
||||||
|
if err := storeToken(token); err != nil {
|
||||||
|
return errors.New(tr.Tf("error.auth.store_token_failed", i18n.Args{"message": err.Error()}))
|
||||||
|
}
|
||||||
|
_, err = fmt.Fprintln(out, tr.T("success.auth.token_saved"))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func newLogoutCmd(tr *i18n.Translator) *cobra.Command {
|
func newLogoutCmd(tr *i18n.Translator) *cobra.Command {
|
||||||
return &cobra.Command{
|
return &cobra.Command{
|
||||||
Use: "logout",
|
Use: "logout",
|
||||||
|
|
|
||||||
|
|
@ -335,3 +335,43 @@ func findSub(cmd *cobra.Command, name string) *cobra.Command {
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoginWithTokenStdin(t *testing.T) {
|
||||||
|
keyring.MockInit()
|
||||||
|
tempConfigDir(t)
|
||||||
|
t.Setenv("GITLINK_TOKEN", "")
|
||||||
|
_ = internalAuth.DeleteToken()
|
||||||
|
|
||||||
|
cmd := findSub(NewAuthCmd(), "login")
|
||||||
|
if cmd == nil {
|
||||||
|
t.Fatal("login subcommand not found")
|
||||||
|
}
|
||||||
|
if err := cmd.Flags().Set("with-token", "true"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
cmd.SetIn(strings.NewReader("stdin-token-456\n"))
|
||||||
|
cmd.SetOut(&buf)
|
||||||
|
if err := cmd.RunE(cmd, nil); err != nil {
|
||||||
|
t.Fatalf("login --with-token error: %v", err)
|
||||||
|
}
|
||||||
|
stored, err := internalAuth.LoadToken()
|
||||||
|
if err != nil || stored != "stdin-token-456" {
|
||||||
|
t.Fatalf("stored token = %q (err %v), want stdin-token-456", stored, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoginWithTokenStdinEmpty(t *testing.T) {
|
||||||
|
keyring.MockInit()
|
||||||
|
tempConfigDir(t)
|
||||||
|
|
||||||
|
cmd := findSub(NewAuthCmd(), "login")
|
||||||
|
if err := cmd.Flags().Set("with-token", "true"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
cmd.SetIn(strings.NewReader("\n"))
|
||||||
|
cmd.SetOut(&bytes.Buffer{})
|
||||||
|
if err := cmd.RunE(cmd, nil); err == nil {
|
||||||
|
t.Fatal("expected error for empty stdin token")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,7 @@
|
||||||
"flag.api.query": "Query parameters (key=val&key2=val2)",
|
"flag.api.query": "Query parameters (key=val&key2=val2)",
|
||||||
"flag.auth.show_token": "Display the raw token in the status output",
|
"flag.auth.show_token": "Display the raw token in the status output",
|
||||||
"flag.auth.token": "Login by pasting an existing token",
|
"flag.auth.token": "Login by pasting an existing token",
|
||||||
|
"flag.auth.with_token": "Read the token from standard input (non-interactive)",
|
||||||
"flag.branch.from": "Source branch or commit",
|
"flag.branch.from": "Source branch or commit",
|
||||||
"flag.branch.name": "Branch name",
|
"flag.branch.name": "Branch name",
|
||||||
"flag.ci.build": "Build number",
|
"flag.ci.build": "Build number",
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,7 @@
|
||||||
"flag.api.query": "查询参数(key=val&key2=val2)",
|
"flag.api.query": "查询参数(key=val&key2=val2)",
|
||||||
"flag.auth.show_token": "在状态输出中显示原始 token",
|
"flag.auth.show_token": "在状态输出中显示原始 token",
|
||||||
"flag.auth.token": "通过粘贴已有 Token 登录",
|
"flag.auth.token": "通过粘贴已有 Token 登录",
|
||||||
|
"flag.auth.with_token": "从标准输入读取令牌(非交互,适合脚本/CI)",
|
||||||
"flag.branch.from": "源分支或 Commit",
|
"flag.branch.from": "源分支或 Commit",
|
||||||
"flag.branch.name": "分支名称",
|
"flag.branch.name": "分支名称",
|
||||||
"flag.ci.build": "构建编号",
|
"flag.ci.build": "构建编号",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue