5
5
"fmt"
6
6
"os"
7
7
"strings"
8
+ "time"
8
9
9
10
"github.com/numtide/nix-auth/internal/config"
10
11
"github.com/numtide/nix-auth/internal/provider"
@@ -13,53 +14,123 @@ import (
13
14
)
14
15
15
16
var loginCmd = & cobra.Command {
16
- Use : "login [provider]" ,
17
+ Use : "login [provider-or-host ]" ,
17
18
Short : "Authenticate with a provider and save the access token" ,
18
- Long : `Authenticate with a provider (GitHub, GitLab, Gitea, Forgejo, etc.) using OAuth device flow
19
- and save the access token to your nix.conf for use with Nix flakes.` ,
20
- Example : ` nix-auth login # defaults to GitHub
19
+ Long : `Authenticate with a provider using OAuth device flow (or Personal Access Token for Gitea/Forgejo)
20
+ and save the access token to your nix.conf for use with Nix flakes.
21
+
22
+ You can specify either:
23
+ - A provider alias (github, gitlab, gitea, codeberg) - uses default host for that provider
24
+ - A host (e.g., github.com, git.company.com) - auto-detects provider type by querying API
25
+
26
+ Notes:
27
+ - The --provider flag only works when specifying a host, not with provider aliases
28
+ - For Forgejo, you must specify a host as it has no default: nix-auth login <host> --provider forgejo
29
+ - Using both a provider alias and --provider flag will result in an error` ,
30
+ Example : ` # Using provider aliases
31
+ nix-auth login # defaults to github
21
32
nix-auth login github
22
33
nix-auth login gitlab
23
34
nix-auth login gitea
24
- nix-auth login codeberg # for codeberg.org
25
- nix-auth login forgejo --host git.company.com # --host required for forgejo
26
- nix-auth login github --host github.company.com --client-id abc123
27
- nix-auth login gitea --host gitea.company.com` ,
28
- Args : cobra .MaximumNArgs (1 ),
29
- RunE : runLogin ,
35
+ nix-auth login codeberg
36
+
37
+ # Using hosts with auto-detection
38
+ nix-auth login github.com
39
+ nix-auth login gitlab.company.com # auto-detects provider type
40
+ nix-auth login git.company.com # auto-detects provider type
41
+
42
+ # Explicit provider specification
43
+ nix-auth login git.company.com --provider forgejo
44
+ nix-auth login github.company.com --client-id abc123` ,
45
+ Args : cobra .MaximumNArgs (1 ),
46
+ RunE : runLogin ,
30
47
}
31
48
32
49
var (
33
- loginHost string
50
+ loginProvider string
34
51
loginClientID string
52
+ loginForce bool
53
+ loginTimeout int
54
+ loginDryRun bool
35
55
)
36
56
37
57
func init () {
38
- loginCmd .Flags ().StringVar (& loginHost , "host" , "" , "Custom host (e.g., github.company.com)" )
39
- loginCmd .Flags ().StringVar (& loginClientID , "client-id" , "" , "OAuth client ID (required for self-hosted instances)" )
58
+ loginCmd .Flags ().StringVar (& loginProvider , "provider" , "auto" , "Provider type when using a host (auto, github, gitlab, gitea, forgejo, codeberg)" )
59
+ loginCmd .Flags ().StringVar (& loginClientID , "client-id" , "" , "OAuth client ID (required for GitHub Enterprise, optional for others)" )
60
+ loginCmd .Flags ().BoolVar (& loginForce , "force" , false , "Skip confirmation prompt when replacing existing tokens" )
61
+ loginCmd .Flags ().IntVar (& loginTimeout , "timeout" , 30 , "Timeout in seconds for network operations" )
62
+ loginCmd .Flags ().BoolVar (& loginDryRun , "dry-run" , false , "Preview what would happen without authenticating" )
40
63
}
41
64
42
65
func runLogin (cmd * cobra.Command , args []string ) error {
43
- providerName := "github" // default
66
+ var host string
67
+ var providerName string
68
+ var prov provider.Provider
69
+
70
+ // Parse the input
71
+ input := "github" // default
44
72
if len (args ) > 0 {
45
- providerName = strings .ToLower (args [0 ])
73
+ input = strings .ToLower (args [0 ])
46
74
}
47
75
48
- // Get provider
49
- prov , ok := provider .Get (providerName )
50
- if ! ok {
51
- available := strings .Join (provider .List (), ", " )
52
- return fmt .Errorf ("unknown provider '%s'. Available providers: %s" , providerName , available )
76
+ // First, determine if we're dealing with a host or provider alias
77
+ isProviderAlias := false
78
+ if _ , ok := provider .Get (input ); ok {
79
+ isProviderAlias = true
80
+ // Check for conflicts
81
+ if loginProvider != "auto" && loginProvider != input {
82
+ return fmt .Errorf ("cannot use --provider %s with provider alias '%s'\n " +
83
+ "Use: nix-auth login %s" , loginProvider , input , input )
84
+ }
53
85
}
54
86
55
- // Determine host
56
- host := prov . Host ()
57
- if loginHost != "" {
58
- host = loginHost
59
- }
87
+ if isProviderAlias {
88
+ // Handle provider alias
89
+ prov , _ = provider . Get ( input )
90
+ providerName = input
91
+ host = prov . Host ()
60
92
61
- // Always set the host (even if it's the default)
62
- prov .SetHost (host )
93
+ // For providers without a default host, require explicit host
94
+ if host == "" {
95
+ return fmt .Errorf ("provider '%s' requires a host\n " +
96
+ "Use: nix-auth login <host> --provider %s" , input , input )
97
+ }
98
+ } else {
99
+ // It's a host
100
+ host = input
101
+
102
+ // Determine the provider
103
+ if loginProvider == "auto" {
104
+ // Auto-detect provider type
105
+ fmt .Printf ("Detecting provider type for %s by querying API...\n " , host )
106
+ ctx , cancel := context .WithTimeout (context .Background (), time .Duration (loginTimeout )* time .Second )
107
+ defer cancel ()
108
+
109
+ detectedProvider , err := provider .DetectProviderFromHost (ctx , host )
110
+ if err != nil {
111
+ return fmt .Errorf ("failed to detect provider for %s: %w\n " +
112
+ "Try: nix-auth login %s --provider <github|gitlab|gitea|forgejo>" ,
113
+ host , err , host )
114
+ }
115
+
116
+ providerName = detectedProvider
117
+ fmt .Printf ("Detected: %s\n \n " , providerName )
118
+ } else {
119
+ // Use explicitly specified provider
120
+ providerName = loginProvider
121
+ }
122
+
123
+ // Get the provider instance
124
+ var ok bool
125
+ prov , ok = provider .Get (providerName )
126
+ if ! ok {
127
+ available := strings .Join (provider .List (), ", " )
128
+ return fmt .Errorf ("unknown provider '%s'. Available providers: %s" , providerName , available )
129
+ }
130
+
131
+ // Set the host on the provider
132
+ prov .SetHost (host )
133
+ }
63
134
64
135
// Set client ID: use flag, fallback to environment variable
65
136
clientID := loginClientID
@@ -78,14 +149,28 @@ func runLogin(cmd *cobra.Command, args []string) error {
78
149
79
150
fmt .Printf ("Authenticating with %s (%s)...\n " , prov .Name (), host )
80
151
152
+ // If dry-run, show what would happen and exit
153
+ if loginDryRun {
154
+ fmt .Println ("\n Dry-run mode: Preview of what would happen:" )
155
+ fmt .Printf ("- Provider: %s\n " , prov .Name ())
156
+ fmt .Printf ("- Host: %s\n " , host )
157
+ fmt .Printf ("- OAuth scopes: %s\n " , strings .Join (prov .GetScopes (), ", " ))
158
+ if clientID != "" {
159
+ fmt .Printf ("- Client ID: %s\n " , clientID )
160
+ }
161
+ fmt .Printf ("- Config file: %s\n " , configPath )
162
+ fmt .Println ("\n No authentication performed. Run without --dry-run to authenticate." )
163
+ return nil
164
+ }
165
+
81
166
// Check if token already exists
82
167
cfg , err := config .New (configPath )
83
168
if err != nil {
84
169
return fmt .Errorf ("failed to initialize config: %w" , err )
85
170
}
86
171
87
172
existingToken , _ := cfg .GetToken (host )
88
- if existingToken != "" {
173
+ if existingToken != "" && ! loginForce {
89
174
fmt .Printf ("A token for %s already exists. Do you want to replace it? [y/N] " , host )
90
175
var response string
91
176
fmt .Scanln (& response )
@@ -96,10 +181,21 @@ func runLogin(cmd *cobra.Command, args []string) error {
96
181
}
97
182
98
183
// Perform authentication
99
- ctx := context .Background ()
184
+ ctx , cancel := context .WithTimeout (context .Background (), time .Duration (loginTimeout )* time .Second )
185
+ defer cancel ()
100
186
token , err := prov .Authenticate (ctx )
101
187
if err != nil {
102
- return fmt .Errorf ("authentication failed: %w" , err )
188
+ errMsg := fmt .Sprintf ("authentication failed: %v" , err )
189
+ if strings .Contains (err .Error (), "context deadline exceeded" ) {
190
+ errMsg += fmt .Sprintf ("\n \n The operation timed out after %d seconds. Try:\n " +
191
+ "- Increasing the timeout: --timeout 60\n " +
192
+ "- Checking your internet connection\n " +
193
+ "- Verifying the host is accessible: curl https://%s" , loginTimeout , host )
194
+ } else if strings .Contains (err .Error (), "client ID" ) {
195
+ errMsg += "\n \n For self-hosted instances, you need to create an OAuth application.\n " +
196
+ "See the instructions above or use --dry-run to preview the configuration."
197
+ }
198
+ return fmt .Errorf (errMsg )
103
199
}
104
200
105
201
// Validate token
0 commit comments