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 }