github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/model/bi/api.go (about)

     1  package bi
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  	"net/url"
    10  	"path"
    11  	"strings"
    12  
    13  	"github.com/cozy/cozy-stack/model/instance"
    14  	"github.com/cozy/cozy-stack/pkg/logger"
    15  	"github.com/cozy/cozy-stack/pkg/safehttp"
    16  )
    17  
    18  type apiClient struct {
    19  	host     string
    20  	basePath string
    21  }
    22  
    23  func newAPIClient(rawURL string) (*apiClient, error) {
    24  	u, err := url.Parse(rawURL)
    25  	if err != nil {
    26  		return nil, err
    27  	}
    28  	if strings.Contains(u.Host, ":") {
    29  		return nil, errors.New("port not allowed in BI url")
    30  	}
    31  	return &apiClient{host: u.Host, basePath: u.Path}, nil
    32  }
    33  
    34  func (c *apiClient) makeRequest(verb, endpointPath, token string, body io.Reader) (*http.Response, error) {
    35  	u := &url.URL{
    36  		Scheme: "https",
    37  		Host:   c.host,
    38  		Path:   path.Join(c.basePath, endpointPath),
    39  	}
    40  	req, err := http.NewRequest(verb, u.String(), body)
    41  	if err != nil {
    42  		return nil, err
    43  	}
    44  	req.Header.Add("Authorization", "Bearer "+token)
    45  	res, err := safehttp.ClientWithKeepAlive.Do(req)
    46  	if err != nil {
    47  		return nil, err
    48  	}
    49  	return res, nil
    50  }
    51  
    52  func (c *apiClient) get(path, token string) (*http.Response, error) {
    53  	return c.makeRequest(http.MethodGet, path, token, nil)
    54  }
    55  
    56  func (c *apiClient) delete(path, token string) (*http.Response, error) {
    57  	return c.makeRequest(http.MethodDelete, path, token, nil)
    58  }
    59  
    60  type connectionsResponse struct {
    61  	Total int `json:"total"`
    62  }
    63  
    64  func (c *apiClient) getNumberOfConnections(inst *instance.Instance, token string) (int, error) {
    65  	res, err := c.get("/users/me/connections", token)
    66  	if err != nil {
    67  		return 0, err
    68  	}
    69  	defer res.Body.Close()
    70  
    71  	if res.StatusCode/100 != 2 {
    72  		return 0, fmt.Errorf("/users/me/connections received response code %d", res.StatusCode)
    73  	}
    74  
    75  	body, err := io.ReadAll(res.Body)
    76  	if err != nil {
    77  		return 0, err
    78  	}
    79  	var data connectionsResponse
    80  	if err := json.Unmarshal(body, &data); err != nil {
    81  		// Truncate the body for the log message if too long
    82  		msg := string(body)
    83  		if len(msg) > 200 {
    84  			msg = msg[0:198] + "..."
    85  		}
    86  		log := inst.Logger().WithNamespace("bi")
    87  		log.Warnf("getNumberOfConnections [%d] cannot parse JSON %s: %s", res.StatusCode, msg, err)
    88  		if log.IsDebug() {
    89  			log.Debugf("getNumberOfConnections called with token %s", token)
    90  			logFullHTML(log, string(body))
    91  		}
    92  		return 0, err
    93  	}
    94  	return data.Total, nil
    95  }
    96  
    97  func logFullHTML(log *logger.Entry, msg string) {
    98  	i := 0
    99  	for len(msg) > 0 {
   100  		idx := len(msg)
   101  		if idx > 1800 {
   102  			idx = 1800
   103  		}
   104  		part := msg[:idx]
   105  		log.Debugf("getNumberOfConnections %d: %s", i, part)
   106  		i++
   107  		msg = msg[idx:]
   108  	}
   109  }
   110  
   111  func (c *apiClient) deleteUser(token string) error {
   112  	res, err := c.delete("/users/me", token)
   113  	if err != nil {
   114  		return err
   115  	}
   116  	defer res.Body.Close()
   117  	if 200 <= res.StatusCode && res.StatusCode < 300 {
   118  		return nil
   119  	}
   120  	return errors.New("invalid response from BI API")
   121  }
   122  
   123  func (c *apiClient) getConnectorUUID(connectionID int, token string) (string, error) {
   124  	path := fmt.Sprintf("/users/me/connections/%d", connectionID)
   125  	res, err := c.get(path, token)
   126  	if err != nil {
   127  		return "", err
   128  	}
   129  	defer res.Body.Close()
   130  
   131  	var data map[string]interface{}
   132  	if err := json.NewDecoder(res.Body).Decode(&data); err != nil {
   133  		return "", err
   134  	}
   135  	if uuid, ok := data["connector_uuid"].(string); ok {
   136  		return uuid, nil
   137  	}
   138  	return "", errors.New("invalid response from BI API")
   139  }