| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284 |
- //go:build ee
- // +build ee
- package billing
- import (
- "crypto/hmac"
- "crypto/sha256"
- "encoding/hex"
- "encoding/json"
- "fmt"
- "io/ioutil"
- "net/http"
- "net/url"
- "strings"
- "time"
- "github.com/gorilla/schema"
- "github.com/porter-dev/porter/api/types"
- cemodels "github.com/porter-dev/porter/internal/models"
- )
- // Client contains an API client for the internal billing engine
- type Client struct {
- apiKey string
- serverURL string
- publicServerURL string
- httpClient *http.Client
- }
- // NewClient creates a new billing API client
- func NewClient(serverURL, publicServerURL, apiKey string) (*Client, error) {
- httpClient := &http.Client{
- Timeout: time.Minute,
- }
- client := &Client{apiKey, serverURL, publicServerURL, httpClient}
- return client, nil
- }
- func (c *Client) CreateTeam(user *cemodels.User, proj *cemodels.Project) (string, error) {
- // call the internal billing endpoint to create a new customer in the database
- reqData := &CreateCustomerRequest{
- Email: user.Email,
- UserID: user.ID,
- ProjectID: proj.ID,
- ProjectName: proj.Name,
- }
- err := c.postRequest("/api/v1/private/customer", reqData, nil)
- if err != nil {
- return "", err
- }
- return fmt.Sprintf("%d-%d", proj.ID, user.ID), nil
- }
- func (c *Client) DeleteTeam(user *cemodels.User, proj *cemodels.Project) error {
- // call delete customer
- reqData := &DeleteCustomerRequest{
- UserID: user.ID,
- ProjectID: proj.ID,
- }
- return c.deleteRequest("/api/v1/private/customer", reqData, nil)
- }
- func (c *Client) GetRedirectURI(user *cemodels.User, proj *cemodels.Project) (string, error) {
- // get an internal cookie
- reqData := &CreateBillingCookieRequest{
- ProjectName: proj.Name,
- ProjectID: proj.ID,
- UserID: user.ID,
- Email: user.Email,
- }
- createCookieVals := make(map[string][]string)
- err := schema.NewEncoder().Encode(reqData, createCookieVals)
- if err != nil {
- return "", err
- }
- urlVals := url.Values(createCookieVals)
- encodedURLVals := urlVals.Encode()
- dst := &CreateBillingCookieResponse{}
- err = c.postRequest("/api/v1/private/cookie", reqData, dst)
- if err != nil {
- return "", err
- }
- redirectData := &VerifyUserRequest{
- TokenID: dst.TokenID,
- Token: dst.Token,
- }
- vals := make(map[string][]string)
- err = schema.NewEncoder().Encode(redirectData, vals)
- if err != nil {
- return "", err
- }
- urlVals = url.Values(vals)
- encodedURLVals = urlVals.Encode()
- return fmt.Sprintf("%s/api/v1/verify?%s", c.publicServerURL, encodedURLVals), nil
- }
- // VerifySignature verifies a webhook signature based on hmac protocol
- func (c *Client) VerifySignature(signature string, body []byte) bool {
- if len(signature) != 71 || !strings.HasPrefix(signature, "sha256=") {
- return false
- }
- actual := make([]byte, 32)
- _, err := hex.Decode(actual, []byte(signature[7:]))
- if err != nil {
- return false
- }
- computed := hmac.New(sha256.New, []byte(c.apiKey))
- _, err = computed.Write(body)
- if err != nil {
- return false
- }
- return hmac.Equal(computed.Sum(nil), actual)
- }
- func (c *Client) postRequest(path string, data interface{}, dst interface{}) error {
- return c.writeRequest("POST", path, data, dst)
- }
- func (c *Client) putRequest(path string, data interface{}, dst interface{}) error {
- return c.writeRequest("PUT", path, data, dst)
- }
- func (c *Client) deleteRequest(path string, data interface{}, dst interface{}) error {
- return c.writeRequest("DELETE", path, data, dst)
- }
- func (c *Client) getRequest(path string, dst interface{}, query ...map[string]string) error {
- reqURL, err := url.Parse(c.serverURL)
- if err != nil {
- return nil
- }
- reqURL.Path = path
- q := reqURL.Query()
- for _, queryGroup := range query {
- for key, val := range queryGroup {
- q.Add(key, val)
- }
- }
- reqURL.RawQuery = q.Encode()
- req, err := http.NewRequest(
- "GET",
- reqURL.String(),
- nil,
- )
- if err != nil {
- return err
- }
- req.Header.Set("Content-Type", "application/json; charset=utf-8")
- req.Header.Set("Accept", "application/json; charset=utf-8")
- req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.apiKey))
- res, err := c.httpClient.Do(req)
- if err != nil {
- return err
- }
- defer res.Body.Close()
- if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest {
- resBytes, err := ioutil.ReadAll(res.Body)
- if err != nil {
- return fmt.Errorf("request failed with status code %d, but could not read body (%s)\n", res.StatusCode, err.Error())
- }
- return fmt.Errorf("request failed with status code %d: %s\n", res.StatusCode, string(resBytes))
- }
- if dst != nil {
- return json.NewDecoder(res.Body).Decode(dst)
- }
- return nil
- }
- func (c *Client) writeRequest(method, path string, data interface{}, dst interface{}) error {
- reqURL, err := url.Parse(c.serverURL)
- if err != nil {
- return nil
- }
- reqURL.Path = path
- var strData []byte
- if data != nil {
- strData, err = json.Marshal(data)
- if err != nil {
- return err
- }
- }
- req, err := http.NewRequest(
- method,
- reqURL.String(),
- strings.NewReader(string(strData)),
- )
- if err != nil {
- return err
- }
- req.Header.Set("Content-Type", "application/json; charset=utf-8")
- req.Header.Set("Accept", "application/json; charset=utf-8")
- req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.apiKey))
- res, err := c.httpClient.Do(req)
- if err != nil {
- return err
- }
- defer res.Body.Close()
- if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest {
- resBytes, err := ioutil.ReadAll(res.Body)
- if err != nil {
- return fmt.Errorf("request failed with status code %d, but could not read body (%s)\n", res.StatusCode, err.Error())
- }
- return fmt.Errorf("request failed with status code %d: %s\n", res.StatusCode, string(resBytes))
- }
- if dst != nil {
- return json.NewDecoder(res.Body).Decode(dst)
- }
- return nil
- }
- const (
- FeatureSlugCPU string = "cpu"
- FeatureSlugMemory string = "memory"
- FeatureSlugClusters string = "clusters"
- FeatureSlugUsers string = "users"
- )
- func (c *Client) ParseProjectUsageFromWebhook(payload []byte) (*cemodels.ProjectUsage, *types.FeatureFlags, error) {
- usageData := &APIWebhookRequest{}
- err := json.Unmarshal(payload, usageData)
- if err != nil {
- return nil, nil, err
- }
- return &cemodels.ProjectUsage{
- ProjectID: usageData.ProjectID,
- ResourceCPU: usageData.CPU,
- ResourceMemory: usageData.Memory * 1000,
- Clusters: usageData.Clusters,
- Users: usageData.Users,
- }, &types.FeatureFlags{
- PreviewEnvironmentsEnabled: usageData.PreviewEnvironmentsEnabled,
- ManagedInfraEnabled: usageData.ManagedInfraEnabled,
- StacksEnabled: usageData.StacksEnabled,
- ManagedDatabasesEnabled: usageData.ManagedDatabasesEnabled,
- CapiProvisionerEnabled: usageData.CapiProvisionerEnabled,
- SimplifiedViewEnabled: usageData.SimplifiedViewEnabled,
- AzureEnabled: usageData.AzureEnabled,
- }, nil
- }
|