Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add widgets client and get token API support #378

Merged
merged 1 commit into from
Nov 15, 2024
Merged
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
15 changes: 15 additions & 0 deletions pkg/widgets/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# widgets

[![Go Report Card](https://img.shields.io/badge/dev-reference-007d9c?logo=go&logoColor=white&style=flat)](https://pkg.go.dev/github.com/workos/workos-go/v4/pkg/widgets)

A Go package to make requests to the WorkOS Widgets API.

## Install

```sh
go get -u github.com/workos/workos-go/v4/pkg/widgets
```

## How it works

See the [Widgets integration guide](https://workos.com/docs/widgets/guide).
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This page doesn't exist yet and should be updated before merging.

115 changes: 115 additions & 0 deletions pkg/widgets/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package widgets

import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"sync"
"time"

"github.com/workos/workos-go/v4/pkg/workos_errors"

"github.com/workos/workos-go/v4/internal/workos"
)

// ResponseLimit is the default number of records to limit a response to.
const ResponseLimit = 10

// Client represents a client that performs Widgets requests to the WorkOS API.
type Client struct {
// The WorkOS API Key. It can be found in https://dashboard.workos.com/api-keys.
APIKey string

// The http.Client that is used to manage Widgets API calls to WorkOS.
// Defaults to http.Client.
HTTPClient *http.Client

// The endpoint to WorkOS API. Defaults to https://api.workos.com.
Endpoint string

// The function used to encode in JSON. Defaults to json.Marshal.
JSONEncode func(v interface{}) ([]byte, error)

once sync.Once
}

func (c *Client) init() {
if c.HTTPClient == nil {
c.HTTPClient = &http.Client{Timeout: 10 * time.Second}
}

if c.Endpoint == "" {
c.Endpoint = "https://api.workos.com"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should ideally be configurable. You can't currently change the API host in the golang SDK, but it's something we should add and that should reflect here too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great point. I also feel like it's less than ideal that we have to redefine this common client logic for every module. Wondering if it should be refactored into a shared module.

}

if c.JSONEncode == nil {
c.JSONEncode = json.Marshal
}
}

// WidgetScope represents a widget token scope.
type WidgetScope string

// Constants that enumerate the available GenerateLinkIntent types.
const (
UsersTableManage WidgetScope = "widgets:users-table:manage"
)

// GetTokenOpts contains the options to get a widget token.
type GetTokenOpts struct {
// Organization identifier to scope the widget token
OrganizationId string `json:"organization_id"`

// AuthKit user identifier to scope the widget token
UserId string `json:"user_id"`

// WidgetScopes to scope the widget token
Scopes []WidgetScope `json:"scopes"`
}

// GetTokenResponse represents the generated widget token
type GetTokenResponse struct {
// Generated widget token
Token string `json:"token"`
}

// GetToken generates a widget token based on the provided options.
func (c *Client) GetToken(
ctx context.Context,
opts GetTokenOpts,
) (string, error) {
c.once.Do(c.init)

data, err := c.JSONEncode(opts)
if err != nil {
return "", err
}

endpoint := fmt.Sprintf("%s/widgets/token", c.Endpoint)
req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer(data))
if err != nil {
return "", err
}
req = req.WithContext(ctx)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+c.APIKey)
req.Header.Set("User-Agent", "workos-go/"+workos.Version)

res, err := c.HTTPClient.Do(req)
if err != nil {
return "", err
}
defer res.Body.Close()

if err = workos_errors.TryGetHTTPError(res); err != nil {
return "", err
}

var body GetTokenResponse

dec := json.NewDecoder(res.Body)
err = dec.Decode(&body)
return body.Token, err
}
78 changes: 78 additions & 0 deletions pkg/widgets/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package widgets

import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/require"
)

func TestGetToken(t *testing.T) {
tests := []struct {
scenario string
client *Client
options GetTokenOpts
expected string
err bool
}{
{
scenario: "Request without API Key returns an error",
client: &Client{},
err: true,
},
{
scenario: "Request returns widget token",
client: &Client{
APIKey: "test",
},
options: GetTokenOpts{
OrganizationId: "organization_id",
UserId: "user_id",
Scopes: []WidgetScope{UsersTableManage},
},
expected: "abc123456",
},
}

for _, test := range tests {
t.Run(test.scenario, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(generateLinkTestHandler))
defer server.Close()

client := test.client
client.Endpoint = server.URL
client.HTTPClient = server.Client()

token, err := client.GetToken(context.Background(), test.options)
if test.err {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Equal(t, test.expected, token)
})
}
}

func generateLinkTestHandler(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization")
if auth != "Bearer test" {
http.Error(w, "bad auth", http.StatusUnauthorized)
return
}

body, err := json.Marshal(struct {
GetTokenResponse
}{GetTokenResponse{Token: "abc123456"}})

if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}

w.WriteHeader(http.StatusOK)
w.Write(body)
}
26 changes: 26 additions & 0 deletions pkg/widgets/widgets.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Package `widgets` provides a client wrapping the WorkOS Widgets API.
package widgets

import (
"context"
)

// DefaultClient is the client used by SetAPIKey and Widgets functions.
var (
DefaultClient = &Client{
Endpoint: "https://api.workos.com",
}
)

// SetAPIKey sets the WorkOS API key for Widgets API requests.
func SetAPIKey(apiKey string) {
DefaultClient.APIKey = apiKey
}

// GetToken generates an ephemeral widget token based on the provided options.
func GetToken(
ctx context.Context,
opts GetTokenOpts,
) (string, error) {
return DefaultClient.GetToken(ctx, opts)
}
32 changes: 32 additions & 0 deletions pkg/widgets/widgets_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package widgets

import (
"context"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/require"
)

func TestWidgetsGetToken(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(generateLinkTestHandler))
defer server.Close()

DefaultClient = &Client{
HTTPClient: server.Client(),
Endpoint: server.URL,
}
SetAPIKey("test")

expectedToken := "abc123456"

token, err := GetToken(context.Background(), GetTokenOpts{
OrganizationId: "organization_id",
UserId: "user_id",
Scopes: []WidgetScope{UsersTableManage},
})

require.NoError(t, err)
require.Equal(t, expectedToken, token)
}
Loading