diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 8a45050..c9c4b7a 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -13,8 +13,9 @@ import ( ) const ( - // DefaultCallbackPort is the port for the OAuth callback server - DefaultCallbackPort = 8080 + // CallbackURL is the OAuth redirect URL that must match the Connected App configuration. + // No server listens on this — the browser shows an error and the user copies the URL. + CallbackURL = "http://localhost:8080/callback" // ProductionLoginURL is the Salesforce production login endpoint ProductionLoginURL = "https://login.salesforce.com" @@ -42,7 +43,7 @@ func GetOAuthConfig(instanceURL, clientID string) *oauth2.Config { AuthURL: instanceURL + "/services/oauth2/authorize", TokenURL: instanceURL + "/services/oauth2/token", }, - RedirectURL: fmt.Sprintf("http://localhost:%d/callback", DefaultCallbackPort), + RedirectURL: CallbackURL, Scopes: Scopes, } } diff --git a/internal/auth/callback.go b/internal/auth/callback.go deleted file mode 100644 index c264968..0000000 --- a/internal/auth/callback.go +++ /dev/null @@ -1,108 +0,0 @@ -package auth - -import ( - "context" - "fmt" - "net" - "net/http" - "time" -) - -// CallbackResult contains the result of an OAuth callback. -type CallbackResult struct { - Code string - Error string -} - -// StartCallbackServer starts a local HTTP server to receive the OAuth callback. -// Returns a channel that will receive the authorization code or error. -func StartCallbackServer(ctx context.Context, port int) (<-chan CallbackResult, error) { - resultChan := make(chan CallbackResult, 1) - - // Check if port is available - listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) - if err != nil { - return nil, fmt.Errorf("failed to start callback server on port %d: %w", port, err) - } - - mux := http.NewServeMux() - mux.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) { - code := r.URL.Query().Get("code") - errMsg := r.URL.Query().Get("error") - errDesc := r.URL.Query().Get("error_description") - - if errMsg != "" { - msg := errMsg - if errDesc != "" { - msg = fmt.Sprintf("%s: %s", errMsg, errDesc) - } - resultChan <- CallbackResult{Error: msg} - - w.Header().Set("Content-Type", "text/html") - fmt.Fprintf(w, ` - -Authentication Failed - -

Authentication Failed

-

Error: %s

-

You can close this window.

- -`, msg) - return - } - - if code == "" { - resultChan <- CallbackResult{Error: "no authorization code received"} - - w.Header().Set("Content-Type", "text/html") - fmt.Fprint(w, ` - -Authentication Failed - -

Authentication Failed

-

No authorization code received.

-

You can close this window.

- -`) - return - } - - resultChan <- CallbackResult{Code: code} - - w.Header().Set("Content-Type", "text/html") - fmt.Fprint(w, ` - -Authentication Successful - -

Authentication Successful!

-

You can close this window and return to the terminal.

- -`) - }) - - server := &http.Server{ - Handler: mux, - ReadTimeout: 30 * time.Second, - WriteTimeout: 30 * time.Second, - } - - // Start server in goroutine - go func() { - if err := server.Serve(listener); err != nil && err != http.ErrServerClosed { - resultChan <- CallbackResult{Error: fmt.Sprintf("callback server error: %v", err)} - } - }() - - // Shutdown server when context is cancelled or result is received - go func() { - select { - case <-ctx.Done(): - case <-resultChan: - } - // Give a moment for the response to be sent - time.Sleep(100 * time.Millisecond) - _ = server.Close() - }() - - return resultChan, nil -} diff --git a/internal/cmd/initcmd/init.go b/internal/cmd/initcmd/init.go index 57e16f8..12b6610 100644 --- a/internal/cmd/initcmd/init.go +++ b/internal/cmd/initcmd/init.go @@ -8,10 +8,7 @@ import ( "net/http" "net/url" "os" - "os/exec" - "runtime" "strings" - "time" "github.com/charmbracelet/huh" "github.com/spf13/cobra" @@ -25,7 +22,6 @@ var ( instanceURL string clientID string noVerify bool - noBrowser bool ) // Register registers the init command with the parent command. @@ -57,7 +53,6 @@ Prerequisites: cmd.Flags().StringVar(&instanceURL, "instance-url", "", "Salesforce instance URL (e.g., login.salesforce.com)") cmd.Flags().StringVar(&clientID, "client-id", "", "Connected App Consumer Key") cmd.Flags().BoolVar(&noVerify, "no-verify", false, "Skip connectivity verification after setup") - cmd.Flags().BoolVar(&noBrowser, "no-browser", false, "Don't auto-open browser, just print URL") return cmd } @@ -165,64 +160,24 @@ func runInit(cmd *cobra.Command, args []string) error { authURL := auth.GetAuthURL(oauthConfig) fmt.Println() - if noBrowser { - fmt.Println("Open this URL in your browser:") - } else { - fmt.Println("Opening browser for Salesforce login...") - fmt.Println() - fmt.Println("If browser doesn't open, visit:") - } + fmt.Println("Open this URL in your browser:") fmt.Println() fmt.Println(authURL) fmt.Println() + fmt.Println("After clicking 'Allow', your browser will redirect to a localhost URL.") + fmt.Println("This will show an error - that's expected!") + fmt.Println() + fmt.Println("Copy the ENTIRE URL from your browser's address bar and paste it here,") + fmt.Println("or just paste the 'code' parameter value:") + fmt.Println() + fmt.Print("> ") - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) - defer cancel() - - resultChan, err := auth.StartCallbackServer(ctx, auth.DefaultCallbackPort) + input, err := reader.ReadString('\n') if err != nil { - fmt.Printf("Warning: Could not start callback server: %v\n", err) - fmt.Println("You'll need to manually copy the authorization code.") - } - - if !noBrowser { - if err := openBrowser(authURL); err != nil { - fmt.Printf("Could not open browser: %v\n", err) - } - } - - var code string - if resultChan != nil { - fmt.Println("Waiting for authorization...") - fmt.Println("(Or paste the authorization code or full redirect URL below)") - fmt.Println() - - inputChan := make(chan string, 1) - go func() { - fmt.Print("> ") - input, _ := reader.ReadString('\n') - inputChan <- strings.TrimSpace(input) - }() - - select { - case result := <-resultChan: - if result.Error != "" { - return fmt.Errorf("authorization failed: %s", result.Error) - } - code = result.Code - fmt.Println("Authorization received from callback.") - case input := <-inputChan: - code = extractAuthCode(input) - case <-ctx.Done(): - return fmt.Errorf("authorization timed out") - } - } else { - fmt.Println("After authorizing, paste the authorization code or full redirect URL:") - fmt.Print("> ") - input, _ := reader.ReadString('\n') - code = extractAuthCode(strings.TrimSpace(input)) + return fmt.Errorf("failed to read input: %w", err) } + code := extractAuthCode(strings.TrimSpace(input)) if code == "" { return fmt.Errorf("no authorization code received") } @@ -230,6 +185,7 @@ func runInit(cmd *cobra.Command, args []string) error { fmt.Println() fmt.Println("Exchanging authorization code for tokens...") + ctx := context.Background() token, err := auth.ExchangeAuthCode(ctx, oauthConfig, code) if err != nil { return fmt.Errorf("failed to exchange authorization code: %w", err) @@ -295,25 +251,3 @@ func verifyConnectivity(instanceURL string) error { return nil } - -// openBrowser opens the default browser to the given URL. -func openBrowser(url string) error { - var cmd string - var args []string - - switch runtime.GOOS { - case "darwin": - cmd = "open" - args = []string{url} - case "linux": - cmd = "xdg-open" - args = []string{url} - case "windows": - cmd = "cmd" - args = []string{"/c", "start", url} - default: - return fmt.Errorf("unsupported platform") - } - - return exec.Command(cmd, args...).Start() -} diff --git a/internal/cmd/initcmd/init_test.go b/internal/cmd/initcmd/init_test.go index 382691c..055ffb4 100644 --- a/internal/cmd/initcmd/init_test.go +++ b/internal/cmd/initcmd/init_test.go @@ -42,6 +42,26 @@ func TestExtractAuthCode(t *testing.T) { input: " abc123 ", want: "abc123", }, + { + name: "localhost URL without port", + input: "http://localhost/?code=abc123xyz", + want: "abc123xyz", + }, + { + name: "https localhost URL", + input: "https://localhost/?code=SecureCode456", + want: "SecureCode456", + }, + { + name: "code with special characters", + input: "http://localhost:8080/callback?code=4/P-abc_123.xyz~456", + want: "4/P-abc_123.xyz~456", + }, + { + name: "URL encoded code", + input: "http://localhost:8080/callback?code=4%2F0AQSTgQ", + want: "4/0AQSTgQ", + }, } for _, tt := range tests { @@ -63,5 +83,7 @@ func TestNewCommand(t *testing.T) { assert.NotNil(t, cmd.Flags().Lookup("instance-url")) assert.NotNil(t, cmd.Flags().Lookup("client-id")) assert.NotNil(t, cmd.Flags().Lookup("no-verify")) - assert.NotNil(t, cmd.Flags().Lookup("no-browser")) + + // --no-browser flag was removed (no more callback server or auto-browser-opening) + assert.Nil(t, cmd.Flags().Lookup("no-browser")) }