Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion cli/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
48 changes: 46 additions & 2 deletions cli/internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -21,6 +23,7 @@ type TokenProvider interface {
type SugaApiClient struct {
tokenProvider TokenProvider
apiUrl *url.URL
debugEnabled bool
}

func NewSugaApiClient(injector do.Injector) (*SugaApiClient, error) {
Expand All @@ -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) {
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
9 changes: 7 additions & 2 deletions cli/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
85 changes: 70 additions & 15 deletions cli/internal/workos/http/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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) {
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion cli/internal/workos/workos.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Loading