diff --git a/cli/cmd/root.go b/cli/cmd/root.go index 3f395fe1..0711c602 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -22,12 +22,17 @@ func NewRootCmd(injector do.Injector) *cobra.Command { return err } + if debugFlag, _ := cmd.Flags().GetBool("debug"); debugFlag { + conf.SetDebug(true) + } + do.ProvideValue(injector, conf) return nil }, } - // Add commands that use the CLI struct methods + rootCmd.PersistentFlags().Bool("debug", false, "Enable debug mode with verbose API request/response logging") + rootCmd.AddCommand(NewLoginCmd(injector)) rootCmd.AddCommand(NewLogoutCmd(injector)) rootCmd.AddCommand(NewAccessTokenCmd(injector)) diff --git a/cli/internal/api/api.go b/cli/internal/api/api.go index b68d39e8..11d643c8 100644 --- a/cli/internal/api/api.go +++ b/cli/internal/api/api.go @@ -5,7 +5,9 @@ import ( "fmt" "io" "net/http" + "net/http/httputil" "net/url" + "os" "github.com/nitrictech/suga/cli/internal/config" "github.com/nitrictech/suga/cli/internal/utils" @@ -21,6 +23,7 @@ type TokenProvider interface { type SugaApiClient struct { tokenProvider TokenProvider apiUrl *url.URL + debugEnabled bool } func NewSugaApiClient(injector do.Injector) (*SugaApiClient, error) { @@ -39,9 +42,34 @@ func NewSugaApiClient(injector do.Injector) (*SugaApiClient, error) { return &SugaApiClient{ apiUrl: apiUrl, tokenProvider: tokenProvider, + debugEnabled: config.Debug, }, nil } +// logRequest logs the HTTP request details if debug mode is enabled +func (c *SugaApiClient) logRequest(req *http.Request) { + if !c.debugEnabled { + return + } + + fmt.Fprintf(os.Stderr, "[DEBUG] API Request:\n") + if dump, err := httputil.DumpRequest(req, true); err == nil { + fmt.Fprintf(os.Stderr, "%s\n\n", dump) + } +} + +// logResponse logs the HTTP response details if debug mode is enabled +func (c *SugaApiClient) logResponse(resp *http.Response) { + if !c.debugEnabled { + return + } + + fmt.Fprintf(os.Stderr, "[DEBUG] API Response:\n") + if dump, err := httputil.DumpResponse(resp, true); err == nil { + fmt.Fprintf(os.Stderr, "%s\n\n", dump) + } +} + // doRequestWithRetry executes an HTTP request and retries once with a refreshed token on 401/403. // Reuses req.Context() and req.GetBody (when available) to rebuild the body. func (c *SugaApiClient) doRequestWithRetry(req *http.Request, requiresAuth bool) (*http.Response, error) { @@ -58,12 +86,18 @@ func (c *SugaApiClient) doRequestWithRetry(req *http.Request, requiresAuth bool) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) } + // Log the request + c.logRequest(req) + // Execute the request resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } + // Log the response + c.logResponse(resp) + // If we got a 401 or 403 and auth is required, try refreshing the token if requiresAuth && (resp.StatusCode == 401 || resp.StatusCode == 403) { resp.Body.Close() // Close the first response body @@ -100,12 +134,22 @@ func (c *SugaApiClient) doRequestWithRetry(req *http.Request, requiresAuth bool) // Copy headers retryReq.Header = req.Header.Clone() - + // Update authorization header with new token retryReq.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + // Log the retry request + c.logRequest(retryReq) + // Retry the request - return http.DefaultClient.Do(retryReq) + retryResp, err := http.DefaultClient.Do(retryReq) + if err != nil { + return nil, err + } + + // Log the retry response + c.logResponse(retryResp) + return retryResp, nil } return resp, nil diff --git a/cli/internal/config/config.go b/cli/internal/config/config.go index 7527f340..6f2d6768 100644 --- a/cli/internal/config/config.go +++ b/cli/internal/config/config.go @@ -14,8 +14,9 @@ import ( ) type Config struct { - v *viper.Viper `mapstructure:"-"` - Url string `mapstructure:"url" desc:"The base URL of the Suga server (e.g., https://app.addsuga.com)"` + v *viper.Viper `mapstructure:"-"` + Url string `mapstructure:"url" desc:"The base URL of the Suga server (e.g., https://app.addsuga.com)"` + Debug bool `mapstructure:"debug" desc:"Enable debug mode with verbose logging"` } func (c *Config) FileUsed() string { @@ -93,6 +94,10 @@ func (c *Config) SetSugaServerUrl(newUrl string) error { return nil } +func (c *Config) SetDebug(debug bool) { + c.Debug = debug +} + func (c *Config) Save(global bool) error { var configMap map[string]interface{} err := mapstructure.Decode(c, &configMap) diff --git a/cli/internal/workos/http/client.go b/cli/internal/workos/http/client.go index 9968f02a..02e24df7 100644 --- a/cli/internal/workos/http/client.go +++ b/cli/internal/workos/http/client.go @@ -11,9 +11,12 @@ import ( "io" "math/big" "net/http" + "net/http/httputil" "net/url" + "os" "path" + "github.com/nitrictech/suga/cli/internal/config" "github.com/nitrictech/suga/cli/internal/utils" ) @@ -96,35 +99,61 @@ type GetAuthorizationUrlOptions struct { // HttpClient represents the WorkOS HTTP client type HttpClient struct { - baseURL string - clientID string - client *http.Client + baseURL string + clientID string + client *http.Client + debugEnabled bool } // NewHttpClient creates a new WorkOS HTTP client -func NewHttpClient(clientID string, options ...ClientOption) *HttpClient { - config := &clientConfig{ +func NewHttpClient(clientID string, cfg *config.Config, options ...ClientOption) *HttpClient { + clientConfig := &clientConfig{ hostname: DEFAULT_HOSTNAME, scheme: "https", } for _, option := range options { - option(config) + option(clientConfig) } baseURL := &url.URL{ - Scheme: config.scheme, - Host: config.hostname, + Scheme: clientConfig.scheme, + Host: clientConfig.hostname, } - if config.port != 0 { - baseURL.Host = fmt.Sprintf("%s:%d", config.hostname, config.port) + if clientConfig.port != 0 { + baseURL.Host = fmt.Sprintf("%s:%d", clientConfig.hostname, clientConfig.port) } return &HttpClient{ - baseURL: baseURL.String(), - clientID: clientID, - client: &http.Client{}, + baseURL: baseURL.String(), + clientID: clientID, + client: &http.Client{}, + debugEnabled: cfg.Debug, + } +} + +// logRequest logs the HTTP request details if debug mode is enabled +func (h *HttpClient) logRequest(req *http.Request) { + if !h.debugEnabled { + return + } + + fmt.Fprintf(os.Stderr, "[DEBUG] WorkOS Request:\n") + if dump, err := httputil.DumpRequest(req, true); err == nil { + fmt.Fprintf(os.Stderr, "%s\n\n", dump) + } +} + +// logResponse logs the HTTP response details if debug mode is enabled +func (h *HttpClient) logResponse(resp *http.Response) { + if !h.debugEnabled { + return + } + + fmt.Fprintf(os.Stderr, "[DEBUG] WorkOS Response:\n") + if dump, err := httputil.DumpResponse(resp, true); err == nil { + fmt.Fprintf(os.Stderr, "%s\n\n", dump) } } @@ -274,7 +303,17 @@ func (h *HttpClient) post(path string, body map[string]interface{}) (*http.Respo req.Header.Set("Content-Type", "application/json") req.Header.Set("x-amz-content-sha256", utils.CalculateSHA256(jsonBody)) - return h.client.Do(req) + // Log the request + h.logRequest(req) + + resp, err := h.client.Do(req) + if err != nil { + return nil, err + } + + // Log the response + h.logResponse(resp) + return resp, nil } func (h *HttpClient) get(path string) (*http.Response, error) { @@ -292,7 +331,17 @@ func (h *HttpClient) get(path string) (*http.Response, error) { req.Header.Set("Accept", "application/json, text/plain, */*") - return h.client.Do(req) + // Log the request + h.logRequest(req) + + resp, err := h.client.Do(req) + if err != nil { + return nil, err + } + + // Log the response + h.logResponse(resp) + return resp, nil } type AuthenticatedWithRefreshTokenOptions struct { @@ -478,12 +527,18 @@ func (c *HttpClient) PollDeviceTokenWithContext(ctx context.Context, deviceCode req.Header.Set("Content-Type", "application/json") req.Header.Set("x-amz-content-sha256", utils.CalculateSHA256(jsonBody)) + // Log the request + c.logRequest(req) + response, err := c.client.Do(req) if err != nil { return nil, fmt.Errorf("failed to make device token request: %w", err) } defer response.Body.Close() + // Log the response + c.logResponse(response) + body, err := io.ReadAll(response.Body) if err != nil { return nil, fmt.Errorf("failed to read device token response: %w", err) diff --git a/cli/internal/workos/workos.go b/cli/internal/workos/workos.go index 552288d5..a1a78aea 100644 --- a/cli/internal/workos/workos.go +++ b/cli/internal/workos/workos.go @@ -52,7 +52,7 @@ func NewWorkOSAuth(inj do.Injector) (*WorkOSAuth, error) { opts = append(opts, http.WithPort(pn)) } } - httpClient := http.NewHttpClient("", opts...) + httpClient := http.NewHttpClient("", config, opts...) tokenStore := do.MustInvokeAs[TokenStore](inj)