Skip to content

Commit

Permalink
Add widgets client and get token API support.
Browse files Browse the repository at this point in the history
  • Loading branch information
mattgd committed Nov 14, 2024
1 parent aa422b4 commit 22d3611
Show file tree
Hide file tree
Showing 5 changed files with 266 additions and 0 deletions.
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).
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"
}

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)
}

0 comments on commit 22d3611

Please sign in to comment.