Skip to content

Commit d873973

Browse files
committed
feat: add set-token command
Useful to set tokens on arbitrary hosts
1 parent f131063 commit d873973

File tree

5 files changed

+642
-5
lines changed

5 files changed

+642
-5
lines changed

cmd/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,5 @@ func init() {
3030
rootCmd.AddCommand(loginCmd)
3131
rootCmd.AddCommand(statusCmd)
3232
rootCmd.AddCommand(logoutCmd)
33+
rootCmd.AddCommand(setTokenCmd)
3334
}

cmd/set_token.go

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
package cmd
2+
3+
import (
4+
"bufio"
5+
"context"
6+
"fmt"
7+
"os"
8+
"strings"
9+
"syscall"
10+
11+
"github.com/numtide/nix-auth/internal/config"
12+
"github.com/numtide/nix-auth/internal/provider"
13+
"github.com/numtide/nix-auth/internal/util"
14+
"github.com/spf13/cobra"
15+
"golang.org/x/term"
16+
)
17+
18+
var (
19+
setTokenForce bool
20+
setTokenProvider string
21+
)
22+
23+
var setTokenCmd = &cobra.Command{
24+
Use: "set-token <host> [token]",
25+
Short: "Set an access token for a specific host",
26+
Long: `Set an access token for a specific host.
27+
28+
The token can be provided as an argument or entered interactively for security.
29+
If a provider is specified or detected, the token will be validated before saving.`,
30+
Example: ` # Set token directly
31+
nix-auth set-token github.com ghp_xxxxxxxxxxxx
32+
33+
# Prompt for token (more secure)
34+
nix-auth set-token github.com
35+
36+
# Force replace existing token
37+
nix-auth set-token github.com ghp_xxxxxxxxxxxx --force
38+
39+
# Specify provider for validation
40+
nix-auth set-token git.company.com --provider gitlab`,
41+
Args: cobra.RangeArgs(1, 2),
42+
RunE: func(cmd *cobra.Command, args []string) error {
43+
ctx := context.Background()
44+
host := args[0]
45+
46+
// Initialize config
47+
cfg, err := config.New(configPath)
48+
if err != nil {
49+
return fmt.Errorf("failed to initialize config: %w", err)
50+
}
51+
52+
// Check if token already exists
53+
hosts, err := cfg.ListTokens()
54+
if err != nil {
55+
return fmt.Errorf("failed to list tokens: %w", err)
56+
}
57+
58+
tokenExists := false
59+
for _, h := range hosts {
60+
if h == host {
61+
tokenExists = true
62+
break
63+
}
64+
}
65+
66+
if tokenExists && !setTokenForce {
67+
existingToken, err := cfg.GetToken(host)
68+
if err == nil && existingToken != "" {
69+
maskedExisting := util.MaskToken(existingToken)
70+
fmt.Printf("Token already exists for %s: %s\n", host, maskedExisting)
71+
fmt.Print("Replace it? (y/N): ")
72+
73+
var response string
74+
fmt.Scanln(&response)
75+
if response != "y" && response != "Y" {
76+
fmt.Println("Operation cancelled")
77+
return nil
78+
}
79+
}
80+
}
81+
82+
// Get token from args or prompt
83+
var token string
84+
if len(args) == 2 {
85+
token = args[1]
86+
} else {
87+
fmt.Printf("Enter token for %s: ", host)
88+
89+
// Check if stdin is a terminal
90+
if term.IsTerminal(int(syscall.Stdin)) {
91+
// Use secure password input for terminals
92+
byteToken, err := term.ReadPassword(int(syscall.Stdin))
93+
fmt.Println() // Add newline after password input
94+
if err != nil {
95+
return fmt.Errorf("failed to read token: %w", err)
96+
}
97+
token = string(byteToken)
98+
} else {
99+
// For non-terminal input (like tests or piped input)
100+
reader := bufio.NewReader(os.Stdin)
101+
tokenBytes, err := reader.ReadString('\n')
102+
if err != nil {
103+
return fmt.Errorf("failed to read token: %w", err)
104+
}
105+
token = strings.TrimSuffix(tokenBytes, "\n")
106+
}
107+
}
108+
109+
// Trim whitespace
110+
token = strings.TrimSpace(token)
111+
if token == "" {
112+
return fmt.Errorf("token cannot be empty")
113+
}
114+
115+
// Determine provider
116+
if setTokenProvider != "" {
117+
// User specified provider
118+
p, ok := provider.Get(setTokenProvider)
119+
if !ok {
120+
return fmt.Errorf("unknown provider: %s", setTokenProvider)
121+
}
122+
// Validate token if provider is available
123+
fmt.Printf("Validating token with %s provider...\n", p.Name())
124+
status, err := p.ValidateToken(ctx, token)
125+
if err != nil {
126+
return fmt.Errorf("token validation failed: %w", err)
127+
}
128+
if status != provider.ValidationStatusValid {
129+
return fmt.Errorf("token is not valid")
130+
}
131+
fmt.Println("Token validated successfully")
132+
} else {
133+
// Try to detect provider from host
134+
p, err := provider.Detect(ctx, host, "")
135+
if err == nil && p.Name() != "unknown" {
136+
// Validate token if provider was detected
137+
fmt.Printf("Detected %s provider, validating token...\n", p.Name())
138+
status, err := p.ValidateToken(ctx, token)
139+
if err != nil {
140+
// Just warn, don't fail
141+
fmt.Printf("Warning: token validation failed: %v\n", err)
142+
} else if status != provider.ValidationStatusValid {
143+
fmt.Printf("Warning: token may not be valid\n")
144+
} else {
145+
fmt.Println("Token validated successfully")
146+
}
147+
}
148+
}
149+
150+
// Set the token
151+
if err := cfg.SetToken(host, token); err != nil {
152+
return fmt.Errorf("failed to set token: %w", err)
153+
}
154+
155+
maskedToken := util.MaskToken(token)
156+
fmt.Printf("Successfully set token for %s: %s\n", host, maskedToken)
157+
fmt.Printf("Config saved to: %s\n", cfg.GetPath())
158+
159+
return nil
160+
},
161+
}
162+
163+
func init() {
164+
setTokenCmd.Flags().BoolVarP(&setTokenForce, "force", "f", false, "Force replace existing token without confirmation")
165+
setTokenCmd.Flags().StringVarP(&setTokenProvider, "provider", "p", "", "Specify provider for token validation (e.g., github, gitlab)")
166+
}

0 commit comments

Comments
 (0)