From 82fcf3a4d2f06e1bae52ac7f7eb0983b0f8f5206 Mon Sep 17 00:00:00 2001 From: Giannis Katsanos Date: Wed, 31 Jan 2024 17:31:47 +0200 Subject: [PATCH] Groundwork for v2 (#206) * chore: Fresh start Removed all files to prepare for a v2 rewrite. * feat: Base for implementing API operations Added core types, methods and interfaces for building API operations. The package provides a basic Backend that can communicate with the Clerk API. The Backend can handle APIRequest requests and APIResponse responses. Types are also provided for API resources and parameters, as well as expected errors. * feat: Add CI workflow Added a new CI workflow for Github Actions that will vet, and lint the code and run the test suite. Linting is done with golangci-lint. Tests are run against all Go versions from 1.19 onwards. --- .github/workflows/ci.yml | 42 ++ .github/workflows/tests.yml | 55 -- README.md | 110 +--- clerk.go | 368 +++++++++++++ clerk/actor_tokens.go | 49 -- clerk/actor_tokens_test.go | 85 --- clerk/allowlists.go | 61 --- clerk/allowlists_test.go | 111 ---- clerk/blocklists.go | 58 --- clerk/blocklists_test.go | 110 ---- clerk/clerk.go | 381 -------------- clerk/clerk_options.go | 38 -- clerk/clerk_options_test.go | 56 -- clerk/clerk_test.go | 276 ---------- clerk/clients.go | 48 -- clerk/clients_test.go | 134 ----- clerk/delete_response.go | 8 - clerk/domains.go | 91 ---- clerk/domains_test.go | 142 ----- clerk/email_addresses.go | 72 --- clerk/email_addresses_test.go | 154 ------ clerk/emails.go | 35 -- clerk/emails_test.go | 57 --- clerk/errors.go | 24 - clerk/http_utils_test.go | 64 --- clerk/instances.go | 133 ----- clerk/instances_test.go | 151 ------ clerk/jwks.go | 23 - clerk/jwks_cache.go | 43 -- clerk/jwks_test.go | 58 --- clerk/jwt_templates.go | 143 ------ clerk/jwt_templates_test.go | 235 --------- clerk/middleware.go | 71 --- clerk/middleware_test.go | 213 -------- clerk/middleware_v2.go | 200 -------- clerk/middleware_v2_test.go | 53 -- clerk/organizations.go | 387 -------------- clerk/organizations_test.go | 343 ------------- clerk/phone_numbers.go | 75 --- clerk/phone_numbers_test.go | 160 ------ clerk/proxy_checks.go | 31 -- clerk/proxy_checks_test.go | 52 -- clerk/redirect_urls.go | 50 -- clerk/redirect_urls_test.go | 106 ---- clerk/saml_connections.go | 140 ----- clerk/saml_connections_test.go | 214 -------- clerk/session_claims.go | 38 -- clerk/session_claims_test.go | 31 -- clerk/sessions.go | 115 ----- clerk/sessions_test.go | 254 --------- clerk/templates.go | 154 ------ clerk/templates_test.go | 227 -------- clerk/tokens.go | 129 ----- clerk/tokens_issuer.go | 38 -- clerk/tokens_options.go | 96 ---- clerk/tokens_options_test.go | 110 ---- clerk/tokens_test.go | 447 ---------------- clerk/users.go | 360 ------------- clerk/users_test.go | 597 ---------------------- clerk/verification.go | 78 --- clerk/verification_test.go | 224 -------- clerk/web3_wallets.go | 8 - clerk/webhooks.go | 38 -- clerk/webhooks_test.go | 104 ---- clerk_test.go | 344 +++++++++++++ docs/clerk-logo-dark.png | Bin 12638 -> 0 bytes docs/clerk-logo-light.png | Bin 4812 -> 0 bytes examples/middleware/main.go | 39 -- examples/operations/main.go | 45 -- go.mod | 14 +- go.sum | 55 +- testdata/200x200-grayscale.jpg | Bin 4757 -> 0 bytes tests/integration/actor_tokens_test.go | 56 -- tests/integration/allowlists_test.go | 40 -- tests/integration/blocklists_test.go | 41 -- tests/integration/clerk_test.go | 40 -- tests/integration/clients_test.go | 34 -- tests/integration/domains_test.go | 68 --- tests/integration/email_addresses_test.go | 75 --- tests/integration/emails_test.go | 48 -- tests/integration/instances_test.go | 57 --- tests/integration/jwt_templates_test.go | 78 --- tests/integration/organizations_test.go | 355 ------------- tests/integration/phone_numbers_test.go | 75 --- tests/integration/proxy_checks_test.go | 31 -- tests/integration/redirect_urls_test.go | 40 -- tests/integration/sessions_test.go | 31 -- tests/integration/templates_test.go | 77 --- tests/integration/users_test.go | 139 ----- 89 files changed, 779 insertions(+), 9561 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/tests.yml create mode 100644 clerk.go delete mode 100644 clerk/actor_tokens.go delete mode 100644 clerk/actor_tokens_test.go delete mode 100644 clerk/allowlists.go delete mode 100644 clerk/allowlists_test.go delete mode 100644 clerk/blocklists.go delete mode 100644 clerk/blocklists_test.go delete mode 100644 clerk/clerk.go delete mode 100644 clerk/clerk_options.go delete mode 100644 clerk/clerk_options_test.go delete mode 100644 clerk/clerk_test.go delete mode 100644 clerk/clients.go delete mode 100644 clerk/clients_test.go delete mode 100644 clerk/delete_response.go delete mode 100644 clerk/domains.go delete mode 100644 clerk/domains_test.go delete mode 100644 clerk/email_addresses.go delete mode 100644 clerk/email_addresses_test.go delete mode 100644 clerk/emails.go delete mode 100644 clerk/emails_test.go delete mode 100644 clerk/errors.go delete mode 100644 clerk/http_utils_test.go delete mode 100644 clerk/instances.go delete mode 100644 clerk/instances_test.go delete mode 100644 clerk/jwks.go delete mode 100644 clerk/jwks_cache.go delete mode 100644 clerk/jwks_test.go delete mode 100644 clerk/jwt_templates.go delete mode 100644 clerk/jwt_templates_test.go delete mode 100644 clerk/middleware.go delete mode 100644 clerk/middleware_test.go delete mode 100644 clerk/middleware_v2.go delete mode 100644 clerk/middleware_v2_test.go delete mode 100644 clerk/organizations.go delete mode 100644 clerk/organizations_test.go delete mode 100644 clerk/phone_numbers.go delete mode 100644 clerk/phone_numbers_test.go delete mode 100644 clerk/proxy_checks.go delete mode 100644 clerk/proxy_checks_test.go delete mode 100644 clerk/redirect_urls.go delete mode 100644 clerk/redirect_urls_test.go delete mode 100644 clerk/saml_connections.go delete mode 100644 clerk/saml_connections_test.go delete mode 100644 clerk/session_claims.go delete mode 100644 clerk/session_claims_test.go delete mode 100644 clerk/sessions.go delete mode 100644 clerk/sessions_test.go delete mode 100644 clerk/templates.go delete mode 100644 clerk/templates_test.go delete mode 100644 clerk/tokens.go delete mode 100644 clerk/tokens_issuer.go delete mode 100644 clerk/tokens_options.go delete mode 100644 clerk/tokens_options_test.go delete mode 100644 clerk/tokens_test.go delete mode 100644 clerk/users.go delete mode 100644 clerk/users_test.go delete mode 100644 clerk/verification.go delete mode 100644 clerk/verification_test.go delete mode 100644 clerk/web3_wallets.go delete mode 100644 clerk/webhooks.go delete mode 100644 clerk/webhooks_test.go create mode 100644 clerk_test.go delete mode 100644 docs/clerk-logo-dark.png delete mode 100644 docs/clerk-logo-light.png delete mode 100644 examples/middleware/main.go delete mode 100644 examples/operations/main.go delete mode 100644 testdata/200x200-grayscale.jpg delete mode 100644 tests/integration/actor_tokens_test.go delete mode 100644 tests/integration/allowlists_test.go delete mode 100644 tests/integration/blocklists_test.go delete mode 100644 tests/integration/clerk_test.go delete mode 100644 tests/integration/clients_test.go delete mode 100644 tests/integration/domains_test.go delete mode 100644 tests/integration/email_addresses_test.go delete mode 100644 tests/integration/emails_test.go delete mode 100644 tests/integration/instances_test.go delete mode 100644 tests/integration/jwt_templates_test.go delete mode 100644 tests/integration/organizations_test.go delete mode 100644 tests/integration/phone_numbers_test.go delete mode 100644 tests/integration/proxy_checks_test.go delete mode 100644 tests/integration/redirect_urls_test.go delete mode 100644 tests/integration/sessions_test.go delete mode 100644 tests/integration/templates_test.go delete mode 100644 tests/integration/users_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..4b087cd1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,42 @@ +name: CI + +on: + pull_request: + push: + branches: + - $default-branch + - v2 + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup go + uses: actions/setup-go@v4 + with: + go-version: "1.21" + - name: Format + run: diff -u <(echo -n) <(gofmt -d -s .) + - name: Vet + run: go vet ./... + - name: Run linter + uses: golangci/golangci-lint-action@v3 + test: + name: "Test: go v${{ matrix.go-version }}" + strategy: + matrix: + go-version: + - "1.19" + - "1.20" + - "1.21" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup go + uses: actions/setup-go@v4 + with: + go-version: ${{ matrix.go-version }} + - name: Run tests + run: go test -v -race ./... diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml deleted file mode 100644 index 52640963..00000000 --- a/.github/workflows/tests.yml +++ /dev/null @@ -1,55 +0,0 @@ -on: - pull_request: - push: - branches: [$default-branch] - -name: tests -env: - GO111MODULE: on - -jobs: - test: - strategy: - matrix: - go-version: [1.16.x] - platform: [ubuntu-latest] - runs-on: ${{ matrix.platform }} - - steps: - - uses: actions/setup-go@v4 - with: - go-version: ${{ matrix.go-version }} - - uses: actions/checkout@v3 - - - name: Cache go modules - uses: actions/cache@v3 - with: - path: ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('go.sum') }} - restore-keys: ${{ runner.os }}-go- - - - name: Run go fmt - if: runner.os != 'Windows' - run: diff -u <(echo -n) <(gofmt -d -s .) - - - name: Run go vet - run: go vet ./... - - - name: Run go test - run: go test -v -race -coverprofile coverage.out -covermode atomic ./... - - - name: Run coverage - if: runner.os != 'Windows' - run: | - EXPECTED_COVER=95 - TOTAL_COVER=`go tool cover -func=coverage.out | grep total | grep -Eo '[0-9]+\.[0-9]+'` - echo "Total coverage was: $TOTAL_COVER %" - echo "Expected coverage: $EXPECTED_COVER %" - - - name: Run integration tests - env: - CLERK_API_URL: ${{ secrets.CLERK_API_URL }} - CLERK_API_KEY: ${{ secrets.CLERK_API_KEY }} - CLERK_SESSION_TOKEN: ${{ secrets.CLERK_SESSION_TOKEN }} - CLERK_SESSION_ID: ${{ secrets.CLERK_SESSION_ID }} - run: go test -tags=integration ./tests/integration diff --git a/README.md b/README.md index fc557a8f..b101b544 100644 --- a/README.md +++ b/README.md @@ -10,15 +10,20 @@ # Clerk Go SDK -Go client library for accessing the [Clerk Backend API](https://clerk.com/docs/reference/backend-api). +**This is still a work in progress. The current stable release is v1. See the main branch for the stable release.** -[![GoDoc](https://img.shields.io/static/v1?label=godoc&message=reference&color=blue)](https://pkg.go.dev/github.com/clerkinc/clerk-sdk-go/clerk) -[![Test Status](https://github.com/clerkinc/clerk-sdk-go/workflows/tests/badge.svg)](https://github.com/clerkinc/clerk-sdk-go/actions?query=workflow%3Atests) +The official Go client library for accessing the [Clerk Backend API](https://clerk.com/docs/reference/backend-api). + +[![GoDoc](https://img.shields.io/static/v1?label=godoc&message=reference&color=blue)](https://pkg.go.dev/github.com/clerk/clerk-sdk-go/v2) [![chat on Discord](https://img.shields.io/discord/856971667393609759.svg?logo=discord)](https://discord.com/invite/b5rXHjAg7A) [![documentation](https://img.shields.io/badge/documentation-clerk-green.svg)](https://clerk.com/docs) [![twitter](https://img.shields.io/twitter/follow/ClerkDev?style=social)](https://twitter.com/intent/follow?screen_name=ClerkDev) +## License + +This SDK is licensed under the MIT license found in the [LICENSE](./LICENSE) file. + --- **Clerk is Hiring!** @@ -26,102 +31,3 @@ Go client library for accessing the [Clerk Backend API](https://clerk.com/docs/r Would you like to work on Open Source software and help maintain this repository? [Apply today!](https://apply.workable.com/clerk-dev/) --- - -## Usage - -First, add the Clerk SDK as a dependency to your project. - -``` -$ go get github.com/clerkinc/clerk-sdk-go -``` - -Add the following import to your Go files. - -```go -import "github.com/clerkinc/clerk-sdk-go/clerk" -``` - -Now, you can create a Clerk client by calling the `clerk.NewClient` function. -This function requires your Clerk API key. -You can get this from the dashboard of your Clerk application. - -Once you have a client, you can use the various services to access different parts of the API. - -```go -apiKey := os.Getenv("CLERK_API_KEY") - -client, err := clerk.NewClient(apiKey) -if err != nil { - // handle error -} - -// List all users for current application -users, err := client.Users().ListAll(clerk.ListAllUsersParams{}) -``` - -The services exposed in the `clerk.Client` divide the API into logical chunks and -follow the same structure that can be found in the [Backend API documentation](https://clerk.com/docs/reference/backend-api). - -For more examples on how to use the client, refer to the [examples](https://github.com/clerkinc/clerk-sdk-go/tree/main/examples/operations) - -### Options - -The SDK `Client` constructor can also accept additional options defined [here](https://github.com/clerk/clerk-sdk-go/blob/main/clerk/clerk_options.go). - -A common use case is injecting your own [`http.Client` object](https://pkg.go.dev/net/http#Client) for testing or automatically retrying requests. -An example using [go-retryablehttp](https://github.com/hashicorp/go-retryablehttp/#getting-a-stdlib-httpclient-with-retries) is shown below: - -```go -retryClient := retryablehttp.NewClient() -retryClient.RetryMax = 5 -standardClient := retryClient.StandardClient() // *http.Client - -clerkSDKClient := clerk.NewClient(token, clerk.WithHTTPClient(standardClient)) -``` - -## Middleware - -The SDK provides the [`WithSessionV2`](https://pkg.go.dev/github.com/clerkinc/clerk-sdk-go/v2/clerk#WithSessionV2) middleware that injects the active session into the request's context. - -The active session's claims can then be accessed using [`SessionFromContext`](https://pkg.go.dev/github.com/clerkinc/clerk-sdk-go/v2/clerk#SessionFromContext). - -```go -mux := http.NewServeMux() -injectActiveSession := clerk.WithSessionV2(client) -mux.Handle("/your-endpoint", injectActiveSession(yourEndpointHandler)) -``` - -Additionally, there's [`RequireSessionV2`](https://pkg.go.dev/github.com/clerkinc/clerk-sdk-go/v2/clerk#RequireSessionV2) that will halt the request and respond with 403 if the user is not authenticated. This can be used to restrict access to certain routes unless the user is authenticated. - -For more info on how to use the middleware, refer to the -[example](https://github.com/clerkinc/clerk-sdk-go/tree/main/examples/middleware). - -### Additional options - -The middleware supports the following options: - -- clerk.WithAuthorizedParty() to set the authorized parties to check against the azp claim of the token -- clerk.WithLeeway() to set a custom leeway that gives some extra time to the token to accommodate for clock skew -- clerk.WithJWTVerificationKey() to set the JWK to use for verifying tokens without the need to fetch or cache any JWKs at runtime -- clerk.WithCustomClaims() to pass a type (e.g. struct), which will be populated with the token claims based on json tags. -- clerk.WithSatelliteDomain() to skip the JWT token's "iss" claim verification. -- clerk.WithProxyURL() to verify the JWT token's "iss" claim against the proxy url. - -For example - -```golang -customClaims := myCustomClaimsStruct{} - -clerk.WithSessionV2( - clerkClient, - clerk.WithAuthorizedParty("my-authorized-party"), - clerk.WithLeeway(5 * time.Second), - clerk.WithCustomClaims(&customClaims), - clerk.WithSatelliteDomain(true), - clerk.WithProxyURL("https://example.com/__clerk"), - ) -``` - -## License - -This SDK is licensed under the MIT license found in the [LICENSE](./LICENSE) file. diff --git a/clerk.go b/clerk.go new file mode 100644 index 00000000..a6f4b2fa --- /dev/null +++ b/clerk.go @@ -0,0 +1,368 @@ +// Package clerk provides a way to communicate with the Clerk API. +// Includes types for Clerk API requests, responses and all +// available resources. +package clerk + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "sync" + "time" +) + +const ( + sdkVersion string = "v2.0.0" + clerkAPIVersion string = "v1" +) + +const ( + // APIURL is the base URL for the Clerk API. + APIURL string = "https://api.clerk.com" +) + +// The Clerk secret key. Configured on a package level. +var secretKey string + +// SetKey sets the Clerk API key. +func SetKey(key string) { + secretKey = key +} + +// APIResource describes a Clerk API resource and contains fields and +// methods common to all resources. +type APIResource struct { + Response *APIResponse `json:"-"` +} + +// Read sets the response on the resource. +func (r *APIResource) Read(response *APIResponse) { + r.Response = response +} + +// APIParams implements functionality that's common to all types +// that can be used as API request parameters. +// It is recommended to embed this type to all types that will be +// used for API operation parameters. +type APIParams struct { +} + +// Add can be used to set parameters to url.Values. The method +// is currently a no-op, but is defined so that all types that +// describe API operation parameters implement the Queryable +// interface. +func (params *APIParams) Add(q url.Values) { +} + +// APIResponse describes responses coming from the Clerk API. +// Exposes some commonly used HTTP response fields along with +// the raw data in the response body. +type APIResponse struct { + Header http.Header + Status string // e.g. "200 OK" + StatusCode int // e.g. 200 + + // TraceID is a unique identifier for tracing the origin of the + // response. + // Useful for debugging purposes. + TraceID string + // RawJSON contains the response body as raw bytes. + RawJSON json.RawMessage +} + +// Success returns true for API response status codes in the +// 200-399 range, false otherwise. +func (resp *APIResponse) Success() bool { + return resp.StatusCode < 400 +} + +// NewAPIResponse creates an APIResponse from the passed http.Response +// and the raw response body. +func NewAPIResponse(resp *http.Response, body json.RawMessage) *APIResponse { + return &APIResponse{ + Header: resp.Header, + TraceID: resp.Header.Get("Clerk-Trace-Id"), + Status: resp.Status, + StatusCode: resp.StatusCode, + RawJSON: body, + } +} + +// APIRequest describes requests to the Clerk API. +type APIRequest struct { + Method string + Path string + Params Queryable +} + +// SetParams sets the APIRequest.Params. +func (req *APIRequest) SetParams(params Queryable) { + req.Params = params +} + +// NewAPIRequest creates an APIRequest with the provided HTTP method +// and path. +func NewAPIRequest(method, path string) *APIRequest { + return &APIRequest{ + Method: method, + Path: path, + } +} + +// Backend is the primary interface for communicating with the Clerk +// API. +type Backend interface { + // Call makes requests to the Clerk API. + Call(context.Context, *APIRequest, ResponseReader) error +} + +// ResponseReader reads Clerk API responses. +type ResponseReader interface { + Read(*APIResponse) +} + +// Queryable can add parameters to url.Values. +// Useful for constructing a request query string. +type Queryable interface { + Add(url.Values) +} + +// BackendConfig is used to configure a new Clerk Backend. +type BackendConfig struct { + // HTTPClient is an HTTP client instance that will be used for + // making API requests. + // If it's not set a default HTTP client will be used. + HTTPClient *http.Client + // URL is the base URL to use for API endpoints. + // If it's not set, the default value for the Backend will be used. + URL *string +} + +// NewBackend returns a default backend implementation with the +// provided configuration. +// Please note that the return type is an interface because the +// Backend is not supposed to be used directly. +func NewBackend(config *BackendConfig) Backend { + if config.HTTPClient == nil { + config.HTTPClient = defaultHTTPClient + } + if config.URL == nil { + config.URL = String(APIURL) + } + return &defaultBackend{ + HTTPClient: config.HTTPClient, + URL: *config.URL, + } +} + +// GetBackend returns the library's supported backend for the Clerk +// API. +func GetBackend() Backend { + var b Backend + + backend.mu.RLock() + b = backend.Backend + backend.mu.RUnlock() + + if b != nil { + return b + } + + b = NewBackend(&BackendConfig{}) + SetBackend(b) + return b +} + +// SetBackend sets the Backend that will be used to make requests +// to the Clerk API. +// Use this method if you need to override the default Backend +// configuration. +func SetBackend(b Backend) { + backend.mu.Lock() + defer backend.mu.Unlock() + backend.Backend = b +} + +type defaultBackend struct { + HTTPClient *http.Client + URL string +} + +// Call sends requests to the Clerk API and handles the responses. +func (b *defaultBackend) Call(ctx context.Context, apiReq *APIRequest, setter ResponseReader) error { + req, err := b.newRequest(ctx, apiReq) + if err != nil { + return err + } + + return b.do(req, apiReq.Params, setter) +} + +func (b *defaultBackend) newRequest(ctx context.Context, apiReq *APIRequest) (*http.Request, error) { + path, err := url.JoinPath(b.URL, clerkAPIVersion, apiReq.Path) + if err != nil { + return nil, err + } + req, err := http.NewRequestWithContext(ctx, apiReq.Method, path, nil) + if err != nil { + return nil, err + } + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", secretKey)) + req.Header.Add("Content-Type", "application/json") + req.Header.Add("User-Agent", fmt.Sprintf("Clerk/%s SDK-Go/%s", clerkAPIVersion, sdkVersion)) + req.Header.Add("X-Clerk-SDK", fmt.Sprintf("go/%s", sdkVersion)) + req = req.WithContext(ctx) + + return req, nil +} + +func (b *defaultBackend) do(req *http.Request, params Queryable, setter ResponseReader) error { + err := setRequestBody(req, params) + if err != nil { + return err + } + + resp, err := b.HTTPClient.Do(req) + if err != nil { + return err + } + resBody, err := io.ReadAll(resp.Body) + defer resp.Body.Close() + if err != nil { + return err + } + + apiResponse := NewAPIResponse(resp, resBody) + // Looks like something went wrong. Handle the error. + if !apiResponse.Success() { + return handleError(apiResponse, resBody) + } + + setter.Read(apiResponse) + err = json.Unmarshal(resBody, setter) + if err != nil { + return err + } + + return nil +} + +// Sets the params in either the request body, or the querystring +// for GET requests. +func setRequestBody(req *http.Request, params Queryable) error { + // GET requests don't have a body, but we will pass the params + // in the query string. + if req.Method == http.MethodGet { + q := req.URL.Query() + params.Add(q) + req.URL.RawQuery = q.Encode() + return nil + } + + body, err := json.Marshal(params) + if err != nil { + return err + } + req.Body = io.NopCloser(bytes.NewReader(body)) + req.GetBody = func() (io.ReadCloser, error) { + return io.NopCloser(bytes.NewReader(body)), nil + } + + return nil +} + +// Error response handling +func handleError(resp *APIResponse, body []byte) error { + apiError := &APIErrorResponse{ + HTTPStatusCode: resp.StatusCode, + } + apiError.Read(resp) + err := json.Unmarshal(body, apiError) + if err != nil || apiError.Errors == nil { + // This is probably not an expected API error. + // Return the raw server response. + return errors.New(string(body)) + } + return apiError +} + +// The active Backend +var backend api + +// This type is a container for a Backend. Guarantees thread-safe +// access to the current Backend. +type api struct { + Backend Backend + mu sync.RWMutex +} + +// defaultHTTPTimeout is the default timeout on the http.Client used +// by the library. +const defaultHTTPTimeout = 5 * time.Second + +// The default HTTP client used for communication with the Clerk API. +var defaultHTTPClient = &http.Client{ + Timeout: defaultHTTPTimeout, +} + +// APIErrorResponse is used for cases where requests to the Clerk +// API result in error responses. +type APIErrorResponse struct { + APIResource + + Errors []Error `json:"errors"` + + HTTPStatusCode int `json:"status,omitempty"` + TraceID string `json:"clerk_trace_id,omitempty"` +} + +// Error returns the marshaled representation of the APIErrorResponse. +func (resp *APIErrorResponse) Error() string { + ret, err := json.Marshal(resp) + if err != nil { + // This shouldn't happen, let's return the raw response + return string(resp.Response.RawJSON) + } + return string(ret) +} + +// Error is a representation of a single error that can occur in the +// Clerk API. +type Error struct { + Code string `json:"code"` + Message string `json:"message"` + LongMessage string `json:"long_message"` + Meta json.RawMessage `json:"meta,omitempty"` +} + +// ListParams holds fields that are common for list API operations. +type ListParams struct { + Limit *int64 `json:"limit,omitempty"` + Offset *int64 `json:"offset,omitempty"` +} + +// Add sets list params to the passed in url.Values. +func (params ListParams) Add(q url.Values) { + if params.Limit != nil { + q.Set("limit", strconv.FormatInt(*params.Limit, 10)) + } + if params.Offset != nil { + q.Set("offset", strconv.FormatInt(*params.Offset, 10)) + } +} + +// String returns a pointer to the provided string value. +func String(v string) *string { + return &v +} + +// Int64 returns a pointer to the provided int64 value. +func Int64(v int64) *int64 { + return &v +} diff --git a/clerk/actor_tokens.go b/clerk/actor_tokens.go deleted file mode 100644 index 6bb9bfc9..00000000 --- a/clerk/actor_tokens.go +++ /dev/null @@ -1,49 +0,0 @@ -package clerk - -import ( - "encoding/json" - "fmt" - "net/http" -) - -type ActorTokenService service - -type ActorTokenResponse struct { - Object string `json:"object"` - ID string `json:"id"` - UserID string `json:"user_id"` - Actor json.RawMessage `json:"actor"` - Token string `json:"token,omitempty"` - Status string `json:"status"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` -} - -type CreateActorTokenParams struct { - UserID string `json:"user_id"` - Actor json.RawMessage `json:"actor"` - ExpiresInSeconds *int `json:"expires_in_seconds"` - SessionMaxDurationInSeconds *int `json:"session_max_duration_in_seconds"` -} - -func (s *ActorTokenService) Create(params CreateActorTokenParams) (*ActorTokenResponse, error) { - req, _ := s.client.NewRequest(http.MethodPost, ActorTokensUrl, ¶ms) - - var actorTokenResponse ActorTokenResponse - _, err := s.client.Do(req, &actorTokenResponse) - if err != nil { - return nil, err - } - return &actorTokenResponse, nil -} - -func (s *ActorTokenService) Revoke(actorTokenID string) (*ActorTokenResponse, error) { - req, _ := s.client.NewRequest(http.MethodPost, fmt.Sprintf("%s/%s/revoke", ActorTokensUrl, actorTokenID)) - - var actorTokenResponse ActorTokenResponse - _, err := s.client.Do(req, &actorTokenResponse) - if err != nil { - return nil, err - } - return &actorTokenResponse, nil -} diff --git a/clerk/actor_tokens_test.go b/clerk/actor_tokens_test.go deleted file mode 100644 index b3f54f94..00000000 --- a/clerk/actor_tokens_test.go +++ /dev/null @@ -1,85 +0,0 @@ -package clerk - -import ( - "encoding/json" - "fmt" - "net/http" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestActorTokenService_CreateIdentifier_happyPath(t *testing.T) { - token := "token" - var actorTokenResponse ActorTokenResponse - _ = json.Unmarshal([]byte(dummyActorTokenJson), &actorTokenResponse) - - client, mux, _, teardown := setup(token) - defer teardown() - - mux.HandleFunc("/actor_tokens", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, http.MethodPost) - testHeader(t, req, "Authorization", "Bearer "+token) - fmt.Fprint(w, dummyActorTokenJson) - }) - - got, _ := client.ActorTokens().Create(CreateActorTokenParams{ - Actor: actorTokenResponse.Actor, - UserID: actorTokenResponse.UserID, - }) - - assert.Equal(t, &actorTokenResponse, got) -} - -func TestActorTokenService_Create_invalidServer(t *testing.T) { - client, _ := NewClient("token") - - _, err := client.ActorTokens().Create(CreateActorTokenParams{ - Actor: []byte(`{"sub":"some_actor_id"}`), - UserID: "some_user_id", - }) - if err == nil { - t.Errorf("Expected error to be returned") - } -} - -func TestActorTokenService_Revoke_happyPath(t *testing.T) { - token := "token" - var actorTokenResponse ActorTokenResponse - _ = json.Unmarshal([]byte(dummyActorTokenJson), &actorTokenResponse) - - client, mux, _, teardown := setup(token) - defer teardown() - - mux.HandleFunc("/actor_tokens/"+actorTokenResponse.ID+"/revoke", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, http.MethodPost) - testHeader(t, req, "Authorization", "Bearer "+token) - _, _ = fmt.Fprint(w, dummyActorTokenJson) - }) - - got, _ := client.ActorTokens().Revoke(actorTokenResponse.ID) - assert.Equal(t, &actorTokenResponse, got) -} - -func TestActorTokenService_Revoke_invalidServer(t *testing.T) { - client, _ := NewClient("token") - - _, err := client.ActorTokens().Revoke("impt_2EKxJqKTYcBMlzMh6BGe2C7kh6b") - if err == nil { - t.Errorf("Expected error to be returned") - } -} - -const dummyActorTokenJson = `{ - "id": "impt_2EKxJqKTYcBMlzMh6BGe2C7kh6b", - "object": "actor_token", - "actor": { - "sub": "some_actor_id", - "iss": "the-issuer" - }, - "user_id": "user_2EKwinD5cZID96QP1ruHsnfGx50", - "status": "pending", - "token": "my-token", - "created_at": 1662358007949, - "updated_at": 1662358007949 -}` diff --git a/clerk/allowlists.go b/clerk/allowlists.go deleted file mode 100644 index 228a614c..00000000 --- a/clerk/allowlists.go +++ /dev/null @@ -1,61 +0,0 @@ -package clerk - -import "net/http" - -type AllowlistsService service - -type AllowlistIdentifierResponse struct { - Object string `json:"object"` - ID string `json:"id"` - InvitationID string `json:"invitation_id,omitempty"` - Identifier string `json:"identifier"` - IdentifierType string `json:"identifier_type"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` -} - -type CreateAllowlistIdentifierParams struct { - Identifier string `json:"identifier"` - Notify bool `json:"notify"` -} - -func (s *AllowlistsService) CreateIdentifier(params CreateAllowlistIdentifierParams) (*AllowlistIdentifierResponse, error) { - req, _ := s.client.NewRequest(http.MethodPost, AllowlistsUrl, ¶ms) - - var allowlistIdentifierResponse AllowlistIdentifierResponse - _, err := s.client.Do(req, &allowlistIdentifierResponse) - if err != nil { - return nil, err - } - return &allowlistIdentifierResponse, nil -} - -func (s *AllowlistsService) DeleteIdentifier(identifierID string) (*DeleteResponse, error) { - req, _ := s.client.NewRequest(http.MethodDelete, AllowlistsUrl+"/"+identifierID) - - var deleteResponse DeleteResponse - _, err := s.client.Do(req, &deleteResponse) - if err != nil { - return nil, err - } - return &deleteResponse, nil -} - -type AllowlistIdentifiersResponse struct { - Data []*AllowlistIdentifierResponse `json:"data"` - TotalCount int64 `json:"total_count"` -} - -func (s *AllowlistsService) ListAllIdentifiers() (*AllowlistIdentifiersResponse, error) { - req, _ := s.client.NewRequest(http.MethodGet, AllowlistsUrl) - - var allowlistIdentifiersResponse []*AllowlistIdentifierResponse - _, err := s.client.Do(req, &allowlistIdentifiersResponse) - if err != nil { - return nil, err - } - return &AllowlistIdentifiersResponse{ - Data: allowlistIdentifiersResponse, - TotalCount: int64(len(allowlistIdentifiersResponse)), - }, nil -} diff --git a/clerk/allowlists_test.go b/clerk/allowlists_test.go deleted file mode 100644 index 89b00ddc..00000000 --- a/clerk/allowlists_test.go +++ /dev/null @@ -1,111 +0,0 @@ -package clerk - -import ( - "encoding/json" - "fmt" - "net/http" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestAllowlistService_CreateIdentifier_happyPath(t *testing.T) { - token := "token" - var allowlistIdentifier AllowlistIdentifierResponse - _ = json.Unmarshal([]byte(dummyAllowlistIdentifierJson), &allowlistIdentifier) - - client, mux, _, teardown := setup(token) - defer teardown() - - mux.HandleFunc("/allowlist_identifiers", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, http.MethodPost) - testHeader(t, req, "Authorization", "Bearer "+token) - fmt.Fprint(w, dummyAllowlistIdentifierJson) - }) - - got, _ := client.Allowlists().CreateIdentifier(CreateAllowlistIdentifierParams{ - Identifier: allowlistIdentifier.Identifier, - }) - - assert.Equal(t, &allowlistIdentifier, got) -} - -func TestAllowlistService_CreateIdentifier_invalidServer(t *testing.T) { - client, _ := NewClient("token") - - _, err := client.Allowlists().CreateIdentifier(CreateAllowlistIdentifierParams{ - Identifier: "dummy@example.com", - }) - if err == nil { - t.Errorf("Expected error to be returned") - } -} - -func TestAllowlistService_DeleteIdentifier_happyPath(t *testing.T) { - token := "token" - var allowlistIdentifier BlocklistIdentifierResponse - _ = json.Unmarshal([]byte(dummyBlocklistIdentifierJson), &allowlistIdentifier) - - client, mux, _, teardown := setup(token) - defer teardown() - - mux.HandleFunc("/allowlist_identifiers/"+allowlistIdentifier.ID, func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, http.MethodDelete) - testHeader(t, req, "Authorization", "Bearer "+token) - response := fmt.Sprintf(`{ "deleted": true, "id": "%s", "object": "allowlist_identifier" }`, allowlistIdentifier.ID) - _, _ = fmt.Fprint(w, response) - }) - - got, _ := client.Allowlists().DeleteIdentifier(allowlistIdentifier.ID) - assert.Equal(t, allowlistIdentifier.ID, got.ID) - assert.True(t, got.Deleted) -} - -func TestAllowlistService_DeleteIdentifier_invalidServer(t *testing.T) { - client, _ := NewClient("token") - - _, err := client.Allowlists().DeleteIdentifier("alid_1mvFol71HiKCcypBd6xxg0IpMBN") - if err == nil { - t.Errorf("Expected error to be returned") - } -} - -func TestAllowlistService_ListAllIdentifiers_happyPath(t *testing.T) { - token := "token" - var allowlistIdentifier AllowlistIdentifierResponse - _ = json.Unmarshal([]byte(dummyAllowlistIdentifierJson), &allowlistIdentifier) - - client, mux, _, teardown := setup(token) - defer teardown() - - mux.HandleFunc("/allowlist_identifiers", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, http.MethodGet) - testHeader(t, req, "Authorization", "Bearer "+token) - fmt.Fprint(w, fmt.Sprintf(`[%s]`, dummyAllowlistIdentifierJson)) - }) - - got, _ := client.Allowlists().ListAllIdentifiers() - - assert.Len(t, got.Data, 1) - assert.Equal(t, int64(1), got.TotalCount) - assert.Equal(t, &allowlistIdentifier, got.Data[0]) -} - -func TestAllowlistService_ListAllIdentifiers_invalidServer(t *testing.T) { - client, _ := NewClient("token") - - _, err := client.Allowlists().ListAllIdentifiers() - if err == nil { - t.Errorf("Expected error to be returned") - } -} - -const dummyAllowlistIdentifierJson = `{ - "id": "alid_1mvFol71HiKCcypBd6xxg0IpMBN", - "object": "allowlist_identifier", - "identifier": "dummy@example.com", - "identifier_type": "email_address", - "invitation_id": "inv_1mvFol71PeRCcypBd628g0IuRmF", - "created_at": 1610783813, - "updated_at": 1610783813 -}` diff --git a/clerk/blocklists.go b/clerk/blocklists.go deleted file mode 100644 index 2b9632b1..00000000 --- a/clerk/blocklists.go +++ /dev/null @@ -1,58 +0,0 @@ -package clerk - -import ( - "net/http" -) - -type BlocklistsService service - -type BlocklistIdentifierResponse struct { - Object string `json:"object"` - ID string `json:"id"` - Identifier string `json:"identifier"` - IdentifierType string `json:"identifier_type"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` -} - -type CreateBlocklistIdentifierParams struct { - Identifier string `json:"identifier"` -} - -func (s *BlocklistsService) CreateIdentifier(params CreateBlocklistIdentifierParams) (*BlocklistIdentifierResponse, error) { - req, _ := s.client.NewRequest(http.MethodPost, BlocklistsUrl, ¶ms) - - var blocklistIdentifierResponse BlocklistIdentifierResponse - _, err := s.client.Do(req, &blocklistIdentifierResponse) - if err != nil { - return nil, err - } - return &blocklistIdentifierResponse, nil -} - -func (s *BlocklistsService) DeleteIdentifier(identifierID string) (*DeleteResponse, error) { - req, _ := s.client.NewRequest(http.MethodDelete, BlocklistsUrl+"/"+identifierID) - - var deleteResponse DeleteResponse - _, err := s.client.Do(req, &deleteResponse) - if err != nil { - return nil, err - } - return &deleteResponse, nil -} - -type BlocklistIdentifiersResponse struct { - Data []*BlocklistIdentifierResponse `json:"data"` - TotalCount int64 `json:"total_count"` -} - -func (s *BlocklistsService) ListAllIdentifiers() (*BlocklistIdentifiersResponse, error) { - req, _ := s.client.NewRequest(http.MethodGet, BlocklistsUrl) - - var blocklistIdentifiersResponse *BlocklistIdentifiersResponse - _, err := s.client.Do(req, &blocklistIdentifiersResponse) - if err != nil { - return nil, err - } - return blocklistIdentifiersResponse, nil -} diff --git a/clerk/blocklists_test.go b/clerk/blocklists_test.go deleted file mode 100644 index 3b3f6fbc..00000000 --- a/clerk/blocklists_test.go +++ /dev/null @@ -1,110 +0,0 @@ -package clerk - -import ( - "encoding/json" - "fmt" - "net/http" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestBlocklistService_CreateIdentifier_happyPath(t *testing.T) { - token := "token" - var blocklistIdentifier BlocklistIdentifierResponse - _ = json.Unmarshal([]byte(dummyBlocklistIdentifierJson), &blocklistIdentifier) - - client, mux, _, teardown := setup(token) - defer teardown() - - mux.HandleFunc("/blocklist_identifiers", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, http.MethodPost) - testHeader(t, req, "Authorization", "Bearer "+token) - fmt.Fprint(w, dummyBlocklistIdentifierJson) - }) - - got, _ := client.Blocklists().CreateIdentifier(CreateBlocklistIdentifierParams{ - Identifier: blocklistIdentifier.Identifier, - }) - - assert.Equal(t, &blocklistIdentifier, got) -} - -func TestBlocklistService_CreateIdentifier_invalidServer(t *testing.T) { - client, _ := NewClient("token") - - _, err := client.Blocklists().CreateIdentifier(CreateBlocklistIdentifierParams{ - Identifier: "dummy@example.com", - }) - if err == nil { - t.Errorf("Expected error to be returned") - } -} - -func TestBlocklistService_DeleteIdentifier_happyPath(t *testing.T) { - token := "token" - var blocklistIdentifier BlocklistIdentifierResponse - _ = json.Unmarshal([]byte(dummyBlocklistIdentifierJson), &blocklistIdentifier) - - client, mux, _, teardown := setup(token) - defer teardown() - - mux.HandleFunc("/blocklist_identifiers/"+blocklistIdentifier.ID, func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, http.MethodDelete) - testHeader(t, req, "Authorization", "Bearer "+token) - response := fmt.Sprintf(`{ "deleted": true, "id": "%s", "object": "blocklist_identifier" }`, blocklistIdentifier.ID) - _, _ = fmt.Fprint(w, response) - }) - - got, _ := client.Blocklists().DeleteIdentifier(blocklistIdentifier.ID) - assert.Equal(t, blocklistIdentifier.ID, got.ID) - assert.True(t, got.Deleted) -} - -func TestBlocklistService_DeleteIdentifier_invalidServer(t *testing.T) { - client, _ := NewClient("token") - - _, err := client.Blocklists().DeleteIdentifier("blid_1mvFol71HiKCcypBd6xxg0IpMBN") - if err == nil { - t.Errorf("Expected error to be returned") - } -} - -func TestBlocklistService_ListAllIdentifiers_happyPath(t *testing.T) { - token := "token" - var blocklistIdentifier BlocklistIdentifierResponse - _ = json.Unmarshal([]byte(dummyBlocklistIdentifierJson), &blocklistIdentifier) - - client, mux, _, teardown := setup(token) - defer teardown() - - mux.HandleFunc("/blocklist_identifiers", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, http.MethodGet) - testHeader(t, req, "Authorization", "Bearer "+token) - fmt.Fprint(w, fmt.Sprintf(`{"data": [%s], "total_count": 1}`, dummyBlocklistIdentifierJson)) - }) - - got, _ := client.Blocklists().ListAllIdentifiers() - - assert.Len(t, got.Data, 1) - assert.Equal(t, int64(1), got.TotalCount) - assert.Equal(t, &blocklistIdentifier, got.Data[0]) -} - -func TestBlocklistService_ListAllIdentifiers_invalidServer(t *testing.T) { - client, _ := NewClient("token") - - _, err := client.Blocklists().ListAllIdentifiers() - if err == nil { - t.Errorf("Expected error to be returned") - } -} - -const dummyBlocklistIdentifierJson = `{ - "id": "blid_1mvFol71HiKCcypBd6xxg0IpMBN", - "object": "blocklist_identifier", - "identifier": "dummy@example.com", - "identifier_type": "email_address", - "created_at": 1610783813, - "updated_at": 1610783813 -}` diff --git a/clerk/clerk.go b/clerk/clerk.go deleted file mode 100644 index f5d125d2..00000000 --- a/clerk/clerk.go +++ /dev/null @@ -1,381 +0,0 @@ -package clerk - -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - "io" - "io/ioutil" - "net/http" - "net/url" - "strconv" - "strings" - "time" -) - -const version = "1.49.0" - -const ( - ProdUrl = "https://api.clerk.dev/v1/" - - ActorTokensUrl = "actor_tokens" - AllowlistsUrl = "allowlist_identifiers" - BlocklistsUrl = "blocklist_identifiers" - ClientsUrl = "clients" - ClientsVerifyUrl = ClientsUrl + "/verify" - DomainsURL = "domains" - EmailAddressesURL = "email_addresses" - EmailsUrl = "emails" - InvitationsURL = "invitations" - OrganizationsUrl = "organizations" - PhoneNumbersURL = "phone_numbers" - ProxyChecksURL = "proxy_checks" - RedirectURLsUrl = "redirect_urls" - SAMLConnectionsUrl = "saml_connections" - SessionsUrl = "sessions" - TemplatesUrl = "templates" - UsersUrl = "users" - UsersCountUrl = UsersUrl + "/count" - WebhooksUrl = "webhooks" - JWTTemplatesUrl = "jwt_templates" -) - -var defaultHTTPClient = &http.Client{Timeout: time.Second * 5} - -type Client interface { - NewRequest(method, url string, body ...interface{}) (*http.Request, error) - Do(req *http.Request, v interface{}) (*http.Response, error) - - DecodeToken(token string) (*TokenClaims, error) - VerifyToken(token string, opts ...VerifyTokenOption) (*SessionClaims, error) - - Allowlists() *AllowlistsService - Blocklists() *BlocklistsService - Clients() *ClientsService - Domains() *DomainsService - EmailAddresses() *EmailAddressesService - Emails() *EmailService - ActorTokens() *ActorTokenService - Instances() *InstanceService - JWKS() *JWKSService - JWTTemplates() *JWTTemplatesService - Organizations() *OrganizationsService - PhoneNumbers() *PhoneNumbersService - ProxyChecks() *ProxyChecksService - RedirectURLs() *RedirectURLsService - SAMLConnections() *SAMLConnectionsService - Sessions() *SessionsService - Templates() *TemplatesService - Users() *UsersService - Webhooks() *WebhooksService - Verification() *VerificationService - Interstitial() ([]byte, error) - - APIKey() string -} - -type service struct { - client Client -} - -type client struct { - client *http.Client - baseURL *url.URL - jwksCache *jwksCache - token string - - allowlists *AllowlistsService - blocklists *BlocklistsService - clients *ClientsService - domains *DomainsService - emailAddresses *EmailAddressesService - emails *EmailService - actorTokens *ActorTokenService - instances *InstanceService - jwks *JWKSService - jwtTemplates *JWTTemplatesService - organizations *OrganizationsService - phoneNumbers *PhoneNumbersService - proxyChecks *ProxyChecksService - redirectURLs *RedirectURLsService - samlConnections *SAMLConnectionsService - sessions *SessionsService - templates *TemplatesService - users *UsersService - webhooks *WebhooksService - verification *VerificationService -} - -// NewClient creates a new Clerk client. -// Because the token supplied will be used for all authenticated requests, -// the created client should not be used across different users -func NewClient(token string, options ...ClerkOption) (Client, error) { - if token == "" { - return nil, errors.New("you must provide an API token") - } - - defaultBaseURL, err := toURLWithEndingSlash(ProdUrl) - if err != nil { - return nil, err - } - - client := &client{ - client: defaultHTTPClient, - baseURL: defaultBaseURL, - token: token, - } - - for _, option := range options { - if err = option(client); err != nil { - return nil, err - } - } - - commonService := &service{client: client} - client.allowlists = (*AllowlistsService)(commonService) - client.blocklists = (*BlocklistsService)(commonService) - client.clients = (*ClientsService)(commonService) - client.domains = (*DomainsService)(commonService) - client.emailAddresses = (*EmailAddressesService)(commonService) - client.emails = (*EmailService)(commonService) - client.actorTokens = (*ActorTokenService)(commonService) - client.instances = (*InstanceService)(commonService) - client.jwks = (*JWKSService)(commonService) - client.jwtTemplates = (*JWTTemplatesService)(commonService) - client.organizations = (*OrganizationsService)(commonService) - client.phoneNumbers = (*PhoneNumbersService)(commonService) - client.proxyChecks = (*ProxyChecksService)(commonService) - client.redirectURLs = (*RedirectURLsService)(commonService) - client.samlConnections = (*SAMLConnectionsService)(commonService) - client.sessions = (*SessionsService)(commonService) - client.templates = (*TemplatesService)(commonService) - client.users = (*UsersService)(commonService) - client.webhooks = (*WebhooksService)(commonService) - client.verification = (*VerificationService)(commonService) - - client.jwksCache = &jwksCache{} - - return client, nil -} - -// Deprecated: NewClientWithBaseUrl is deprecated. Use the NewClient instead e.g. NewClient(token, WithBaseURL(baseUrl)) -func NewClientWithBaseUrl(token, baseUrl string) (Client, error) { - return NewClient(token, WithBaseURL(baseUrl)) -} - -// Deprecated: NewClientWithCustomHTTP is deprecated. Use the NewClient instead e.g. NewClient(token, WithBaseURL(urlStr), WithHTTPClient(httpClient)) -func NewClientWithCustomHTTP(token, urlStr string, httpClient *http.Client) (Client, error) { - return NewClient(token, WithBaseURL(urlStr), WithHTTPClient(httpClient)) -} - -func toURLWithEndingSlash(u string) (*url.URL, error) { - baseURL, err := url.Parse(u) - if err != nil { - return nil, err - } - - if !strings.HasSuffix(baseURL.Path, "/") { - baseURL.Path += "/" - } - - return baseURL, err -} - -// NewRequest creates an API request. -// A relative URL `url` can be specified which is resolved relative to the baseURL of the client. -// Relative URLs should be specified without a preceding slash. -// The `body` parameter can be used to pass a body to the request. If no body is required, the parameter can be omitted. -func (c *client) NewRequest(method, url string, body ...interface{}) (*http.Request, error) { - fullUrl, err := c.baseURL.Parse(url) - if err != nil { - return nil, err - } - - var buf io.ReadWriter - if len(body) > 0 && body[0] != nil { - buf = &bytes.Buffer{} - enc := json.NewEncoder(buf) - enc.SetEscapeHTML(false) - err := enc.Encode(body[0]) - if err != nil { - return nil, err - } - } - - req, err := http.NewRequest(method, fullUrl.String(), buf) - if err != nil { - return nil, err - } - - // Add custom header with the current SDK version - req.Header.Set("X-Clerk-SDK", fmt.Sprintf("go/%s", version)) - - return req, nil -} - -// Do will send the given request using the client `c` on which it is called. -// If the response contains a body, it will be unmarshalled in `v`. -func (c *client) Do(req *http.Request, v interface{}) (*http.Response, error) { - req.Header.Set("Authorization", "Bearer "+c.token) - - resp, err := c.client.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - err = checkForErrors(resp) - if err != nil { - return resp, err - } - - if resp.Body != nil && v != nil { - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return resp, err - } - - err = json.Unmarshal(body, &v) - if err != nil { - return resp, err - } - } - - return resp, nil -} - -func checkForErrors(resp *http.Response) error { - if c := resp.StatusCode; c >= 200 && c < 400 { - return nil - } - - errorResponse := &ErrorResponse{Response: resp} - - data, err := ioutil.ReadAll(resp.Body) - if err == nil && data != nil { - // it's ok if we cannot unmarshal to Clerk's error response - _ = json.Unmarshal(data, errorResponse) - } - - return errorResponse -} - -func (c *client) Allowlists() *AllowlistsService { - return c.allowlists -} - -func (c *client) Blocklists() *BlocklistsService { - return c.blocklists -} - -func (c *client) Clients() *ClientsService { - return c.clients -} - -func (c *client) Domains() *DomainsService { - return c.domains -} - -func (c *client) EmailAddresses() *EmailAddressesService { - return c.emailAddresses -} - -func (c *client) Emails() *EmailService { - return c.emails -} - -func (c *client) ActorTokens() *ActorTokenService { - return c.actorTokens -} - -func (c *client) Instances() *InstanceService { - return c.instances -} - -func (c *client) JWKS() *JWKSService { - return c.jwks -} - -func (c *client) JWTTemplates() *JWTTemplatesService { - return c.jwtTemplates -} - -func (c *client) Organizations() *OrganizationsService { - return c.organizations -} - -func (c *client) PhoneNumbers() *PhoneNumbersService { - return c.phoneNumbers -} - -func (c *client) ProxyChecks() *ProxyChecksService { - return c.proxyChecks -} - -func (c *client) RedirectURLs() *RedirectURLsService { - return c.redirectURLs -} - -func (c *client) SAMLConnections() *SAMLConnectionsService { - return c.samlConnections -} - -func (c *client) Sessions() *SessionsService { - return c.sessions -} - -func (c *client) Templates() *TemplatesService { - return c.templates -} - -func (c *client) Users() *UsersService { - return c.users -} - -func (c *client) Webhooks() *WebhooksService { - return c.webhooks -} - -func (c *client) Verification() *VerificationService { - return c.verification -} - -func (c *client) APIKey() string { - return c.token -} - -func (c *client) Interstitial() ([]byte, error) { - req, err := c.NewRequest("GET", "internal/interstitial") - if err != nil { - return nil, err - } - req.Header.Set("Authorization", "Bearer "+c.token) - - resp, err := c.client.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - interstitial, err := ioutil.ReadAll(resp.Body) - if err != nil { - return interstitial, err - } - - return interstitial, nil -} - -type PaginationParams struct { - Limit *int - Offset *int -} - -func addPaginationParams(query url.Values, params PaginationParams) { - if params.Limit != nil { - query.Set("limit", strconv.Itoa(*params.Limit)) - } - if params.Offset != nil { - query.Set("offset", strconv.Itoa(*params.Offset)) - } -} diff --git a/clerk/clerk_options.go b/clerk/clerk_options.go deleted file mode 100644 index 4c9a2d6d..00000000 --- a/clerk/clerk_options.go +++ /dev/null @@ -1,38 +0,0 @@ -package clerk - -import ( - "errors" - "net/http" -) - -// ClerkOption describes a functional parameter for the clerk client constructor -type ClerkOption func(*client) error - -// WithHTTPClient allows the overriding of the http client -func WithHTTPClient(httpClient *http.Client) ClerkOption { - return func(c *client) error { - if httpClient == nil { - return errors.New("http client can't be nil") - } - - c.client = httpClient - return nil - } -} - -// WithBaseURL allows the overriding of the base URL -func WithBaseURL(rawURL string) ClerkOption { - return func(c *client) error { - if rawURL == "" { - return errors.New("base url can't be empty") - } - - baseURL, err := toURLWithEndingSlash(rawURL) - if err != nil { - return err - } - - c.baseURL = baseURL - return nil - } -} diff --git a/clerk/clerk_options_test.go b/clerk/clerk_options_test.go deleted file mode 100644 index 13cb6ede..00000000 --- a/clerk/clerk_options_test.go +++ /dev/null @@ -1,56 +0,0 @@ -package clerk - -import ( - "net/http" - "testing" - "time" -) - -func TestWithHTTPClient(t *testing.T) { - expectedHTTPClient := &http.Client{Timeout: time.Second * 10} - - got, err := NewClient("token", WithHTTPClient(expectedHTTPClient)) - if err != nil { - t.Fatal(err) - } - - if got.(*client).client != expectedHTTPClient { - t.Fatalf("Expected the http client to have been overriden") - } -} - -func TestWithHTTPClientNil(t *testing.T) { - _, err := NewClient("token", WithHTTPClient(nil)) - if err == nil { - t.Fatalf("Expected an error with a nil http client provided") - } -} - -func TestWithBaseURL(t *testing.T) { - expectedBaseURL := "https://api.example.com/" - - got, err := NewClient("token", WithBaseURL(expectedBaseURL)) - if err != nil { - t.Fatal(err) - } - - if got.(*client).baseURL.String() != expectedBaseURL { - t.Fatalf("Expected the base URL to have been overriden") - } -} - -func TestWithBaseURLEmpty(t *testing.T) { - _, err := NewClient("token", WithBaseURL("")) - if err == nil { - t.Fatalf("Expected an error with an empty base URL provided") - } -} - -func TestWithBaseURLInvalid(t *testing.T) { - invalidBaseURL := "https:// api.example.com" - - _, err := NewClient("token", WithBaseURL(invalidBaseURL)) - if err == nil { - t.Fatalf("Expected an error with an invalid base URL provided") - } -} diff --git a/clerk/clerk_test.go b/clerk/clerk_test.go deleted file mode 100644 index 729f16c8..00000000 --- a/clerk/clerk_test.go +++ /dev/null @@ -1,276 +0,0 @@ -package clerk - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "net/url" - "reflect" - "testing" -) - -func TestNewClient_baseUrl(t *testing.T) { - c, err := NewClient("token") - if err != nil { - t.Errorf("NewClient failed") - } - - if got, want := c.(*client).baseURL.String(), ProdUrl; got != want { - t.Errorf("NewClient BaseURL is %v, want %v", got, want) - } -} - -func TestNewClient_baseUrlWithoutSlash(t *testing.T) { - input, want := "http://host/v1", "http://host/v1/" - c, _ := NewClient("token", WithBaseURL(input)) - - if got := c.(*client).baseURL.String(); got != want { - t.Errorf("NewClient BaseURL is %v, want %v", got, want) - } -} - -func TestNewClient_createsDifferentClients(t *testing.T) { - httpClient1, httpClient2 := &http.Client{}, &http.Client{} - - token := "token" - c1, _ := NewClient(token, WithHTTPClient(httpClient1)) - c2, _ := NewClient(token, WithHTTPClient(httpClient2)) - - if c1.(*client).client == c2.(*client).client { - t.Error("NewClient returned same http.Clients, but they should differ") - } -} - -func TestNewRequest(t *testing.T) { - client, _ := NewClient("token") - - inputUrl, outputUrl := "test", ProdUrl+"test" - method := "GET" - req, err := client.NewRequest(method, inputUrl) - if err != nil { - t.Errorf("NewRequest(%q, %s) method is generated error %v", inputUrl, method, err) - } - - if got, want := req.Method, method; got != want { - t.Errorf("NewRequest(%q, %s) method is %v, want %v", inputUrl, method, got, want) - } - - if got, want := req.URL.String(), outputUrl; got != want { - t.Errorf("NewRequest(%q, %s) URL is %v, want %v", inputUrl, method, got, want) - } -} - -func TestNewRequest_invalidUrl(t *testing.T) { - client, _ := NewClient("token") - _, err := client.NewRequest("GET", ":") - if err == nil { - t.Errorf("Expected error to be returned") - } - if err, ok := err.(*url.Error); !ok || err.Op != "parse" { - t.Errorf("Expected URL parse error, got %+v", err) - } -} - -func TestNewRequest_invalidMethod(t *testing.T) { - client, _ := NewClient("token") - invalidMethod := "ΠΟΣΤ" - _, err := client.NewRequest(invalidMethod, "/test") - if err == nil { - t.Errorf("Expected error to be returned") - } -} - -func TestNewRequest_noBody(t *testing.T) { - client, _ := NewClient("token") - req, _ := client.NewRequest("GET", ".") - if req.Body != nil { - t.Fatalf("Expected nil Body but request contains a non-nil Body") - } -} - -func TestNewRequest_nilBody(t *testing.T) { - client, _ := NewClient("token") - req, _ := client.NewRequest("GET", ".", nil) - if req.Body != nil { - t.Fatalf("Expected nil Body but request contains a non-nil Body") - } -} - -func TestNewRequest_withBody(t *testing.T) { - client, _ := NewClient("token") - - type Foo struct { - Key string `json:"key"` - } - - inBody, outBody := Foo{Key: "value"}, `{"key":"value"}`+"\n" - req, _ := client.NewRequest("GET", ".", inBody) - - body, _ := ioutil.ReadAll(req.Body) - if got, want := string(body), outBody; got != want { - t.Errorf("NewRequest(%q) Body is %v, want %v", inBody, got, want) - } -} - -func TestNewRequest_invalidBody(t *testing.T) { - client, _ := NewClient("token") - - _, err := client.NewRequest("GET", ".", make(chan int)) - if err == nil { - t.Errorf("Expected error to be returned") - } -} - -func TestDo_happyPath(t *testing.T) { - client, mux, _, teardown := setup("token") - defer teardown() - - type foo struct { - A string - } - - mux.HandleFunc("/test", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "GET") - fmt.Fprint(w, `{"A":"a"}`) - }) - - req, _ := client.NewRequest("GET", "test") - body := new(foo) - client.Do(req, body) - - want := &foo{"a"} - if !reflect.DeepEqual(body, want) { - t.Errorf("Response body = %v, want %v", body, want) - } -} - -func TestDo_sendsTokenInRequest(t *testing.T) { - token := "token" - client, mux, _, teardown := setup(token) - defer teardown() - - mux.HandleFunc("/test", func(w http.ResponseWriter, req *http.Request) { - testHeader(t, req, "Authorization", "Bearer "+token) - w.WriteHeader(http.StatusNoContent) - }) - - req, _ := client.NewRequest("GET", "test") - _, err := client.Do(req, nil) - if err != nil { - t.Errorf("Was not expecting any errors") - } -} - -func TestDo_invalidServer(t *testing.T) { - client, _ := NewClient("token", WithBaseURL("http://dummy_url:1337")) - - req, _ := client.NewRequest("GET", "test") - - // No server setup, should result in an error - _, err := client.Do(req, nil) - if err == nil { - t.Errorf("Expected error to be returned") - } -} - -func TestDo_handlesClerkErrors(t *testing.T) { - client, mux, _, teardown := setup("token") - defer teardown() - - expected := &ErrorResponse{ - Errors: []Error{{ - Message: "Error message", - LongMessage: "Error long message", - Code: "error_message", - }}, - } - - mux.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusBadRequest) - data, _ := json.Marshal(expected) - w.Write(data) - }) - - req, _ := client.NewRequest("GET", "test") - resp, err := client.Do(req, nil) - - if err == nil { - t.Fatal("Expected HTTP 400 error, got no error.") - } - if resp.StatusCode != 400 { - t.Fatalf("Expected HTTP 400 error, got %d status code.", resp.StatusCode) - } - - errorResponse, isClerkErr := err.(*ErrorResponse) - if !isClerkErr { - t.Fatal("Expected Clerk error response.") - } - if errorResponse.Response != nil { - t.Fatal("Expected error response to contain the HTTP response") - } - if !reflect.DeepEqual(errorResponse.Errors, expected.Errors) { - t.Fatalf("Actual = %v, want %v", errorResponse.Errors, expected.Errors) - } -} - -func TestDo_unexpectedHttpError(t *testing.T) { - client, mux, _, teardown := setup("token") - defer teardown() - - mux.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(500) - }) - - req, _ := client.NewRequest("GET", "test") - resp, err := client.Do(req, nil) - - if err == nil { - t.Fatal("Expected HTTP 500 error, got no error.") - } - if resp.StatusCode != 500 { - t.Errorf("Expected HTTP 500 error, got %d status code.", resp.StatusCode) - } -} - -func TestDo_failToReadBody(t *testing.T) { - client, mux, _, teardown := setup("token") - defer teardown() - - type foo struct { - A string - } - - mux.HandleFunc("/test", func(w http.ResponseWriter, req *http.Request) { - // Lying about the body, telling client the length is 1 but not sending anything back - w.Header().Set("Content-Length", "1") - }) - - req, _ := client.NewRequest("GET", "test") - body := new(foo) - _, err := client.Do(req, body) - if err == nil { - t.Fatal("Expected EOF error, got no error.") - } -} - -func TestDo_failToUnmarshalBody(t *testing.T) { - client, mux, _, teardown := setup("token") - defer teardown() - - type foo struct { - A string - } - - mux.HandleFunc("/test", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "GET") - fmt.Fprint(w, `{invalid}`) - }) - - req, _ := client.NewRequest("GET", "test") - body := new(foo) - _, err := client.Do(req, body) - if err == nil { - t.Fatal("Expected JSON encoding error, got no error.") - } -} diff --git a/clerk/clients.go b/clerk/clients.go deleted file mode 100644 index 6f6d8979..00000000 --- a/clerk/clients.go +++ /dev/null @@ -1,48 +0,0 @@ -package clerk - -import "fmt" - -type ClientsService service - -type ClientResponse struct { - Object string `json:"object"` - ID string `json:"id"` - LastActiveSessionID *string `json:"last_active_session_id"` - SessionIDs []string `json:"session_ids"` - Sessions []*Session `json:"sessions"` - Ended bool `json:"ended"` -} - -func (s *ClientsService) ListAll() ([]ClientResponse, error) { - clientsUrl := "clients" - req, _ := s.client.NewRequest("GET", clientsUrl) - - var clients []ClientResponse - _, err := s.client.Do(req, &clients) - if err != nil { - return nil, err - } - return clients, nil -} - -func (s *ClientsService) Read(clientId string) (*ClientResponse, error) { - clientUrl := fmt.Sprintf("%s/%s", ClientsUrl, clientId) - req, _ := s.client.NewRequest("GET", clientUrl) - - var clientResponse ClientResponse - _, err := s.client.Do(req, &clientResponse) - if err != nil { - return nil, err - } - return &clientResponse, nil -} - -func (s *ClientsService) Verify(token string) (*ClientResponse, error) { - var clientResponse ClientResponse - - err := doVerify(s.client, ClientsVerifyUrl, token, &clientResponse) - if err != nil { - return nil, err - } - return &clientResponse, nil -} diff --git a/clerk/clients_test.go b/clerk/clients_test.go deleted file mode 100644 index ef4da2d5..00000000 --- a/clerk/clients_test.go +++ /dev/null @@ -1,134 +0,0 @@ -package clerk - -import ( - "encoding/json" - "fmt" - "net/http" - "reflect" - "testing" -) - -func TestClientsService_ListAll_happyPath(t *testing.T) { - client, mux, _, teardown := setup("token") - defer teardown() - - expectedResponse := "[" + dummyClientResponseJson + "]" - - mux.HandleFunc("/clients", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "GET") - testHeader(t, req, "Authorization", "Bearer token") - fmt.Fprint(w, expectedResponse) - }) - - var want []ClientResponse - _ = json.Unmarshal([]byte(expectedResponse), &want) - - got, _ := client.Clients().ListAll() - if len(got) != len(want) { - t.Errorf("Was expecting %d clients to be returned, instead got %d", len(want), len(got)) - } - - if !reflect.DeepEqual(got, want) { - t.Errorf("Response = %v, want %v", got, want) - } -} - -func TestClientsService_ListAll_invalidServer(t *testing.T) { - client, _ := NewClient("token") - - response, err := client.Clients().ListAll() - if err == nil { - t.Errorf("Expected error to be returned") - } - if response != nil { - t.Errorf("Was not expecting any clients to be returned, instead got %v", response) - } -} - -func TestClientsService_Read_happyPath(t *testing.T) { - token := "token" - clientId := "someClientId" - expectedResponse := dummyClientResponseJson - - client, mux, _, teardown := setup(token) - defer teardown() - - mux.HandleFunc("/clients/"+clientId, func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "GET") - testHeader(t, req, "Authorization", "Bearer "+token) - fmt.Fprint(w, expectedResponse) - }) - - var want ClientResponse - _ = json.Unmarshal([]byte(expectedResponse), &want) - - got, _ := client.Clients().Read(clientId) - if !reflect.DeepEqual(*got, want) { - t.Errorf("Response = %v, want %v", *got, want) - } -} - -func TestClientsService_Read_invalidServer(t *testing.T) { - client, _ := NewClient("token") - - response, err := client.Clients().Read("someClientId") - if err == nil { - t.Errorf("Expected error to be returned") - } - if response != nil { - t.Errorf("Was not expecting any client to be returned, instead got %v", response) - } -} - -func TestClientsService_Verify_happyPath(t *testing.T) { - token := "token" - sessionToken := "sessionToken" - expectedResponse := dummyClientResponseJson - - client, mux, _, teardown := setup(token) - defer teardown() - - mux.HandleFunc("/clients/verify", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "POST") - testHeader(t, req, "Authorization", "Bearer "+token) - fmt.Fprint(w, expectedResponse) - }) - - var want ClientResponse - _ = json.Unmarshal([]byte(expectedResponse), &want) - - got, _ := client.Clients().Verify(sessionToken) - if !reflect.DeepEqual(*got, want) { - t.Errorf("Response = %v, want %v", *got, want) - } -} - -func TestClientsService_Verify_invalidServer(t *testing.T) { - client, _ := NewClient("token") - - response, err := client.Clients().Verify("someSessionToken") - if err == nil { - t.Errorf("Expected error to be returned") - } - if response != nil { - t.Errorf("Was not expecting any client to be returned, instead got %v", response) - } -} - -const dummyClientResponseJson = `{ - "ended": false, - "id": "client_1mvnkzXhKhn9pDjp1f4x1id6pQZ", - "last_active_session_id": "sess_1mebQdHlQI14cjxln4e2eXNzwzi", - "session_ids": ["sess_1mebQdHlQI14cjxln4e2eXNzwzi"], - "sessions": [{ - "id": "sess_1mebQdHlQI14cjxln4e2eXNzwzi", - "abandon_at": 1612448988, - "client_id": "client_1mebPYz8NFNA17fi7NemNXIwp1p", - "expire_at": 1610461788, - "last_active_at": 1609857251, - "object": "session", - "status": "ended", - "user_id": "user_1mebQggrD3xO5JfuHk7clQ94ysA" - }], - "object": "client" - }` diff --git a/clerk/delete_response.go b/clerk/delete_response.go deleted file mode 100644 index 3bb080e8..00000000 --- a/clerk/delete_response.go +++ /dev/null @@ -1,8 +0,0 @@ -package clerk - -type DeleteResponse struct { - ID string `json:"id,omitempty"` - Slug string `json:"slug,omitempty"` - Object string `json:"object"` - Deleted bool `json:"deleted"` -} diff --git a/clerk/domains.go b/clerk/domains.go deleted file mode 100644 index ed0890a1..00000000 --- a/clerk/domains.go +++ /dev/null @@ -1,91 +0,0 @@ -package clerk - -import ( - "fmt" - "net/http" -) - -type DomainsService service - -type DomainListResponse struct { - Data []Domain `json:"data"` - TotalCount int `json:"total_count"` -} - -type DomainCNameTarget struct { - Host string `json:"host"` - Value string `json:"value"` -} - -type Domain struct { - Object string `json:"object"` - ID string `json:"id"` - Name string `json:"name"` - IsSatellite bool `json:"is_satellite"` - FapiURL string `json:"frontend_api_url"` - AccountsPortalURL *string `json:"accounts_portal_url,omitempty"` - ProxyURL *string `json:"proxy_url,omitempty"` - CNameTargets []DomainCNameTarget `json:"cname_targets,omitempty"` - DevelopmentOrigin string `json:"development_origin"` -} - -type CreateDomainParams struct { - Name string `json:"name"` - IsSatellite bool `json:"is_satellite"` - ProxyURL *string `json:"proxy_url,omitempty"` -} - -type UpdateDomainParams struct { - Name *string `json:"name,omitempty"` - ProxyURL *string `json:"proxy_url,omitempty"` -} - -func (s *DomainsService) ListAll() (*DomainListResponse, error) { - req, _ := s.client.NewRequest(http.MethodGet, DomainsURL) - - var response *DomainListResponse - _, err := s.client.Do(req, &response) - if err != nil { - return nil, err - } - return response, nil -} - -func (s *DomainsService) Create( - params CreateDomainParams, -) (*Domain, error) { - req, _ := s.client.NewRequest(http.MethodPost, DomainsURL, ¶ms) - - var domain Domain - _, err := s.client.Do(req, &domain) - if err != nil { - return nil, err - } - return &domain, nil -} - -func (s *DomainsService) Update( - domainID string, - params UpdateDomainParams, -) (*Domain, error) { - url := fmt.Sprintf("%s/%s", DomainsURL, domainID) - req, _ := s.client.NewRequest(http.MethodPatch, url, ¶ms) - - var domain Domain - _, err := s.client.Do(req, &domain) - if err != nil { - return nil, err - } - return &domain, nil -} - -func (s *DomainsService) Delete(domainID string) (*DeleteResponse, error) { - url := fmt.Sprintf("%s/%s", DomainsURL, domainID) - req, _ := s.client.NewRequest(http.MethodDelete, url) - - var delResponse DeleteResponse - if _, err := s.client.Do(req, &delResponse); err != nil { - return nil, err - } - return &delResponse, nil -} diff --git a/clerk/domains_test.go b/clerk/domains_test.go deleted file mode 100644 index c6375adc..00000000 --- a/clerk/domains_test.go +++ /dev/null @@ -1,142 +0,0 @@ -package clerk - -import ( - "encoding/json" - "fmt" - "net/http" - "reflect" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestDomainsService_ListAll_HappyPath(t *testing.T) { - client, mux, _, teardown := setup("token") - defer teardown() - - expectedResponse := fmt.Sprintf(`{ - "total_count": 1, - "data": [%s] - }`, domainJSON) - - mux.HandleFunc("/domains", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "GET") - testHeader(t, req, "Authorization", "Bearer token") - fmt.Fprint(w, expectedResponse) - }) - - var want *DomainListResponse - _ = json.Unmarshal([]byte(expectedResponse), &want) - - got, _ := client.Domains().ListAll() - if got.TotalCount != want.TotalCount { - t.Errorf("Was expecting %d domains to be returned, instead got %d", want.TotalCount, got.TotalCount) - } - - if !reflect.DeepEqual(got, want) { - t.Errorf("Response = %v, want %v", got, want) - } -} - -func TestDomainsService_Create_HappyPath(t *testing.T) { - token := "token" - expectedResponse := domainJSON - - name := "foobar.com" - - payload := CreateDomainParams{ - Name: name, - IsSatellite: true, - } - - client, mux, _, teardown := setup(token) - defer teardown() - - mux.HandleFunc("/domains", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "POST") - testHeader(t, req, "Authorization", "Bearer "+token) - fmt.Fprint(w, expectedResponse) - }) - - var want Domain - err := json.Unmarshal([]byte(expectedResponse), &want) - assert.Nil(t, err) - - got, err := client.Domains().Create(payload) - assert.Nil(t, err) - - if !reflect.DeepEqual(*got, want) { - t.Errorf("Response = %v, want %v", got, want) - } -} - -func TestDomainsService_Update_HappyPath(t *testing.T) { - token := "token" - domainID := "dmn_banana" - expectedResponse := domainJSON - - name := "foobar.com" - - payload := UpdateDomainParams{ - Name: &name, - } - - client, mux, _, teardown := setup(token) - defer teardown() - - url := fmt.Sprintf("/domains/%s", domainID) - - mux.HandleFunc(url, func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "PATCH") - testHeader(t, req, "Authorization", "Bearer "+token) - fmt.Fprint(w, expectedResponse) - }) - - var want Domain - err := json.Unmarshal([]byte(expectedResponse), &want) - assert.Nil(t, err) - - got, err := client.Domains().Update(domainID, payload) - assert.Nil(t, err) - - if !reflect.DeepEqual(*got, want) { - t.Errorf("Response = %v, want %v", got, want) - } -} - -func TestDomainsService_Delete_HappyPath(t *testing.T) { - token := "token" - domainID := "dmn_banana" - expectedResponse := `{ - "object": "domain", - "id": "dmn_banana", - "delete": "true" - }` - - client, mux, _, teardown := setup(token) - defer teardown() - - url := fmt.Sprintf("/domains/%s", domainID) - - mux.HandleFunc(url, func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "DELETE") - testHeader(t, req, "Authorization", "Bearer "+token) - fmt.Fprint(w, expectedResponse) - }) - - var want DeleteResponse - err := json.Unmarshal([]byte(expectedResponse), &want) - assert.Nil(t, err) - - got, err := client.Domains().Delete(domainID) - assert.Nil(t, err) - - if !reflect.DeepEqual(*got, want) { - t.Errorf("Response = %#v, want %#v", got, want) - } -} - -const domainJSON = `{ - "id": "dmn_banana", - "name": "foobar.com" -}` diff --git a/clerk/email_addresses.go b/clerk/email_addresses.go deleted file mode 100644 index def3b81f..00000000 --- a/clerk/email_addresses.go +++ /dev/null @@ -1,72 +0,0 @@ -package clerk - -import "fmt" - -type EmailAddressesService service - -type EmailAddress struct { - ID string `json:"id"` - Object string `json:"object"` - EmailAddress string `json:"email_address"` - Reserved bool `json:"reserved"` - Verification *Verification `json:"verification"` - LinkedTo []IdentificationLink `json:"linked_to"` -} - -type CreateEmailAddressParams struct { - UserID string `json:"user_id"` - EmailAddress string `json:"email_address"` - Verified *bool `json:"verified"` - Primary *bool `json:"primary"` -} - -type UpdateEmailAddressParams struct { - Verified *bool `json:"verified"` - Primary *bool `json:"primary"` -} - -func (s *EmailAddressesService) Create(params CreateEmailAddressParams) (*EmailAddress, error) { - req, _ := s.client.NewRequest("POST", EmailAddressesURL, ¶ms) - - var emailAddress EmailAddress - _, err := s.client.Do(req, &emailAddress) - if err != nil { - return nil, err - } - return &emailAddress, nil -} - -func (s *EmailAddressesService) Read(emailAddressID string) (*EmailAddress, error) { - emailAddressURL := fmt.Sprintf("%s/%s", EmailAddressesURL, emailAddressID) - req, _ := s.client.NewRequest("GET", emailAddressURL) - - var emailAddress EmailAddress - _, err := s.client.Do(req, &emailAddress) - if err != nil { - return nil, err - } - return &emailAddress, nil -} - -func (s *EmailAddressesService) Update(emailAddressID string, params UpdateEmailAddressParams) (*EmailAddress, error) { - emailAddressURL := fmt.Sprintf("%s/%s", EmailAddressesURL, emailAddressID) - req, _ := s.client.NewRequest("PATCH", emailAddressURL, ¶ms) - - var emailAddress EmailAddress - _, err := s.client.Do(req, &emailAddress) - if err != nil { - return nil, err - } - return &emailAddress, nil -} - -func (s *EmailAddressesService) Delete(emailAddressID string) (*DeleteResponse, error) { - emailAddressURL := fmt.Sprintf("%s/%s", EmailAddressesURL, emailAddressID) - req, _ := s.client.NewRequest("DELETE", emailAddressURL) - - var delResponse DeleteResponse - if _, err := s.client.Do(req, &delResponse); err != nil { - return nil, err - } - return &delResponse, nil -} diff --git a/clerk/email_addresses_test.go b/clerk/email_addresses_test.go deleted file mode 100644 index 6e1e2f8f..00000000 --- a/clerk/email_addresses_test.go +++ /dev/null @@ -1,154 +0,0 @@ -package clerk - -import ( - "encoding/json" - "fmt" - "github.com/stretchr/testify/assert" - "net/http" - "reflect" - "testing" -) - -func TestEmailAddressesService_Create_HappyPath(t *testing.T) { - token := "token" - expectedResponse := unverifiedEmailAddressJSON - - verified := false - primary := false - - payload := CreateEmailAddressParams{ - UserID: "user_abcdefg", - EmailAddress: "banana@cherry.com", - Verified: &verified, - Primary: &primary, - } - - client, mux, _, teardown := setup(token) - defer teardown() - - url := "/email_addresses" - - mux.HandleFunc(url, func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "POST") - testHeader(t, req, "Authorization", "Bearer "+token) - fmt.Fprint(w, expectedResponse) - }) - - var want EmailAddress - _ = json.Unmarshal([]byte(expectedResponse), &want) - - got, err := client.EmailAddresses().Create(payload) - assert.Nil(t, err) - - if !reflect.DeepEqual(*got, want) { - t.Errorf("Response = %v, want %v", got, want) - } -} - -func TestEmailAddressesService_Read_HappyPath(t *testing.T) { - token := "token" - emailAddressID := "idn_banana" - expectedResponse := unverifiedEmailAddressJSON - - client, mux, _, teardown := setup(token) - defer teardown() - - url := fmt.Sprintf("/email_addresses/%s", emailAddressID) - - mux.HandleFunc(url, func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "GET") - testHeader(t, req, "Authorization", "Bearer "+token) - fmt.Fprint(w, expectedResponse) - }) - - var want EmailAddress - _ = json.Unmarshal([]byte(expectedResponse), &want) - - got, err := client.EmailAddresses().Read(emailAddressID) - assert.Nil(t, err) - - if !reflect.DeepEqual(*got, want) { - t.Errorf("Response = %v, want %v", got, want) - } -} - -func TestEmailAddressesService_Update_HappyPath(t *testing.T) { - token := "token" - emailAddressID := "idn_banana" - expectedResponse := verifiedEmailAddressJSON - - verified := true - primary := true - - payload := UpdateEmailAddressParams{ - Verified: &verified, - Primary: &primary, - } - - client, mux, _, teardown := setup(token) - defer teardown() - - url := fmt.Sprintf("/email_addresses/%s", emailAddressID) - - mux.HandleFunc(url, func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "PATCH") - testHeader(t, req, "Authorization", "Bearer "+token) - fmt.Fprint(w, expectedResponse) - }) - - var want EmailAddress - _ = json.Unmarshal([]byte(expectedResponse), &want) - - got, err := client.EmailAddresses().Update(emailAddressID, payload) - assert.Nil(t, err) - - if !reflect.DeepEqual(*got, want) { - t.Errorf("Response = %v, want %v", got, want) - } -} - -func TestEmailAddressesService_Delete_HappyPath(t *testing.T) { - token := "token" - emailAddressID := "idn_banana" - - client, mux, _, teardown := setup(token) - defer teardown() - - url := fmt.Sprintf("/email_addresses/%s", emailAddressID) - - mux.HandleFunc(url, func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "DELETE") - testHeader(t, req, "Authorization", "Bearer "+token) - response := fmt.Sprintf(`{ "deleted": true, "id": "%v", "object": "email_address" }`, emailAddressID) - fmt.Fprint(w, response) - }) - - want := DeleteResponse{ID: emailAddressID, Object: "email_address", Deleted: true} - - got, _ := client.EmailAddresses().Delete(emailAddressID) - if !reflect.DeepEqual(*got, want) { - t.Errorf("Response = %v, want %v", *got, want) - } -} - -const unverifiedEmailAddressJSON = `{ - "id": "idn_banana", - "object": "email_address", - "email_address": "banana@cherry.com", - "reserved": true, - "linked_to": [] -}` - -const verifiedEmailAddressJSON = `{ - "id": "idn_banana", - "object": "email_address", - "email_address": "banana@cherry.com", - "reserved": true, - "verification": { - "status": "verified", - "strategy": "admin", - "attempts": null, - "expire_at": null - }, - "linked_to": [] -}` diff --git a/clerk/emails.go b/clerk/emails.go deleted file mode 100644 index 8b0b3aa3..00000000 --- a/clerk/emails.go +++ /dev/null @@ -1,35 +0,0 @@ -package clerk - -import ( - "encoding/json" -) - -type EmailService service - -type Email struct { - FromEmailName string `json:"from_email_name"` - Subject string `json:"subject"` - Body string `json:"body"` - EmailAddressID string `json:"email_address_id"` -} - -type EmailResponse struct { - ID string `json:"id"` - Object string `json:"object"` - Status string `json:"status,omitempty"` - ToEmailAddress *string `json:"to_email_address,omitempty"` - DeliveredByClerk bool `json:"delivered_by_clerk"` - Data json.RawMessage `json:"data"` - Email -} - -func (s *EmailService) Create(email Email) (*EmailResponse, error) { - req, _ := s.client.NewRequest("POST", EmailsUrl, &email) - - var emailResponse EmailResponse - _, err := s.client.Do(req, &emailResponse) - if err != nil { - return nil, err - } - return &emailResponse, nil -} diff --git a/clerk/emails_test.go b/clerk/emails_test.go deleted file mode 100644 index 3f85e2f6..00000000 --- a/clerk/emails_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package clerk - -import ( - "encoding/json" - "fmt" - "net/http" - "reflect" - "testing" -) - -func TestEmailService_Create_happyPath(t *testing.T) { - token := "token" - var email Email - _ = json.Unmarshal([]byte(dummyEmailJson), &email) - - client, mux, _, teardown := setup(token) - defer teardown() - - mux.HandleFunc("/emails", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "POST") - testHeader(t, req, "Authorization", "Bearer "+token) - fmt.Fprint(w, dummyEmailJson) - }) - - got, _ := client.Emails().Create(email) - - var want EmailResponse - _ = json.Unmarshal([]byte(dummyEmailJson), &want) - - if !reflect.DeepEqual(*got, want) { - t.Errorf("Response = %v, want %v", *got, email) - } -} - -func TestEmailService_Create_invalidServer(t *testing.T) { - client, _ := NewClient("token") - - var email Email - _ = json.Unmarshal([]byte(dummyEmailJson), &email) - - _, err := client.Emails().Create(email) - if err == nil { - t.Errorf("Expected error to be returned") - } -} - -const dummyEmailJson = `{ - "body": "This is the body of a test email", - "email_address_id": "idn_1mebQ9KkZWrhb9rL6iEiXQGF8Yj", - "from_email_name": "info", - "id": "ema_1mvFol71HiKCcypBd6xxg0IpMBN", - "object": "email", - "status": "queued", - "subject": "This is a test email", - "data": { "foo": "bar" }, - "delivered_by_clerk": false -}` diff --git a/clerk/errors.go b/clerk/errors.go deleted file mode 100644 index bbf8324c..00000000 --- a/clerk/errors.go +++ /dev/null @@ -1,24 +0,0 @@ -package clerk - -import ( - "fmt" - "net/http" -) - -type ErrorResponse struct { - Response *http.Response - Errors []Error `json:"errors"` -} - -type Error struct { - Message string `json:"message"` - LongMessage string `json:"long_message"` - Code string `json:"code"` - Meta interface{} `json:"meta,omitempty"` -} - -func (e *ErrorResponse) Error() string { - return fmt.Sprintf("%v %v: %d %+v", - e.Response.Request.Method, e.Response.Request.URL, - e.Response.StatusCode, e.Errors) -} diff --git a/clerk/http_utils_test.go b/clerk/http_utils_test.go deleted file mode 100644 index c6955411..00000000 --- a/clerk/http_utils_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package clerk - -import ( - "net/http" - "net/http/httptest" - "net/url" - "testing" -) - -// setup sets up a test HTTP server along with a `clerk.Client` that is configured to talk to that test server. -// Tests should register handlers on mux which provide mock responses for the API method being tested. -func setup(token string) (client Client, mux *http.ServeMux, serverURL *url.URL, teardown func()) { - versionPath := "/v1" - - mux = http.NewServeMux() - apiHandler := http.NewServeMux() - apiHandler.Handle(versionPath+"/", http.StripPrefix(versionPath, mux)) - - // server is a test HTTP server used to provide mock API responses. - server := httptest.NewServer(apiHandler) - - baseURL, _ := url.Parse(server.URL + versionPath + "/") - client, _ = NewClient(token, WithBaseURL(baseURL.String())) - - return client, mux, baseURL, server.Close -} - -func testHttpMethod(t *testing.T, r *http.Request, want string) { - t.Helper() - if got := r.Method; got != want { - t.Errorf("Request method: %v, want %v", got, want) - } -} - -func testHeader(t *testing.T, r *http.Request, header, want string) { - t.Helper() - if got := r.Header.Get(header); got != want { - t.Errorf("Header.Get(%q) returned %q, want %q", header, got, want) - } -} - -func testQuery(t *testing.T, r *http.Request, want url.Values) { - t.Helper() - - query := r.URL.Query() - - for k := range want { - if query.Get(k) == "" { - t.Errorf("Request query doesn't match: have %v, want %v", query, want) - } - } -} - -func stringToPtr(input string) *string { - return &input -} - -func intToPtr(input int) *int { - return &input -} - -func int64ToPtr(input int64) *int64 { - return &input -} diff --git a/clerk/instances.go b/clerk/instances.go deleted file mode 100644 index 037e925a..00000000 --- a/clerk/instances.go +++ /dev/null @@ -1,133 +0,0 @@ -package clerk - -import ( - "net/http" -) - -type InstanceService service - -type UpdateInstanceParams struct { - // TestMode can be used to toggle test mode for this instance. - // Defaults to true for development instances. - TestMode *bool `json:"test_mode,omitempty"` - - // HIBP is used to configure whether Clerk should use the - // "Have I Been Pawned" service to check passwords against - // known security breaches. - // By default, this is enabled in all instances. - HIBP *bool `json:"hibp,omitempty"` - - // EnhancedEmailDeliverability controls how Clerk delivers emails. - // Specifically, when set to true, if the instance is a production - // instance, OTP verification emails are sent by the Clerk's shared - // domain via Postmark. - EnhancedEmailDeliverability *bool `json:"enhanced_email_deliverability,omitempty"` - - // SupportEmail is the contact email address that will be displayed - // on the frontend, in case your instance users need support. - // If the empty string is provided, the support email that is currently - // configured in the instance will be removed. - SupportEmail *string `json:"support_email,omitempty"` - - // ClerkJSVersion allows you to request a specific Clerk JS version on the Clerk Hosted Account pages. - // If an empty string is provided, the stored version will be removed. - // If an explicit version is not set, the Clerk JS version will be automatically be resolved. - ClerkJSVersion *string `json:"clerk_js_version,omitempty"` - - // CookielessDev can be used to enable the new mode in which no third-party - // cookies are used in development instances. Make sure to also enable the - // setting in Clerk.js - // - // Deprecated: Use URLBasedSessionSyncing instead - CookielessDev *bool `json:"cookieless_dev,omitempty"` - - // URLBasedSessionSyncing can be used to enable the new mode in which no third-party - // cookies are used in development instances. Make sure to also enable the - // setting in Clerk.js - URLBasedSessionSyncing *bool `json:"url_based_session_syncing,omitempty"` - - // URL that is going to be used in development instances in order to create custom redirects - // and fix the third-party cookies issues. - DevelopmentOrigin *string `json:"development_origin,omitempty"` -} - -func (s *InstanceService) Update(params UpdateInstanceParams) error { - req, _ := s.client.NewRequest(http.MethodPatch, "instance", ¶ms) - - _, err := s.client.Do(req, nil) - return err -} - -type InstanceRestrictionsResponse struct { - Object string `json:"object"` - Allowlist bool `json:"allowlist"` - Blocklist bool `json:"blocklist"` - BlockEmailSubaddresses bool `json:"block_email_subaddresses"` - BlockDisposableEmailDomains bool `json:"block_disposable_email_domains"` -} - -type UpdateRestrictionsParams struct { - Allowlist *bool `json:"allowlist,omitempty"` - Blocklist *bool `json:"blocklist,omitempty"` - BlockEmailSubaddresses *bool `json:"block_email_subaddresses,omitempty"` - BlockDisposableEmailDomains *bool `json:"block_disposable_email_domains,omitempty"` -} - -func (s *InstanceService) UpdateRestrictions(params UpdateRestrictionsParams) (*InstanceRestrictionsResponse, error) { - req, _ := s.client.NewRequest(http.MethodPatch, "instance/restrictions", ¶ms) - - var instanceRestrictionsResponse InstanceRestrictionsResponse - _, err := s.client.Do(req, &instanceRestrictionsResponse) - if err != nil { - return nil, err - } - return &instanceRestrictionsResponse, nil -} - -type OrganizationSettingsResponse struct { - Object string `json:"object"` - Enabled bool `json:"enabled"` - MaxAllowedMemberships int `json:"max_allowed_memberships"` - MaxAllowedRoles int `json:"max_allowed_roles"` - MaxAllowedPermissions int `json:"max_allowed_permissions"` - CreatorRole string `json:"creator_role"` - AdminDeleteEnabled bool `json:"admin_delete_enabled"` - DomainsEnabled bool `json:"domains_enabled"` - DomainsEnrollmentModes []string `json:"domains_enrollment_modes"` - DomainsDefaultRole string `json:"domains_default_role"` -} - -type UpdateOrganizationSettingsParams struct { - Enabled *bool `json:"enabled,omitempty"` - MaxAllowedMemberships *int `json:"max_allowed_memberships,omitempty"` - CreatorRoleID *string `json:"creator_role_id,omitempty"` - AdminDeleteEnabled *bool `json:"admin_delete_enabled,omitempty"` - DomainsEnabled *bool `json:"domains_enabled,omitempty"` - DomainsEnrollmentModes []string `json:"domains_enrollment_modes,omitempty"` - DomainsDefaultRoleID *string `json:"domains_default_role_id,omitempty"` -} - -func (s *InstanceService) UpdateOrganizationSettings(params UpdateOrganizationSettingsParams) (*OrganizationSettingsResponse, error) { - req, _ := s.client.NewRequest(http.MethodPatch, "instance/organization_settings", ¶ms) - - var organizationSettingsResponse OrganizationSettingsResponse - _, err := s.client.Do(req, &organizationSettingsResponse) - if err != nil { - return nil, err - } - return &organizationSettingsResponse, nil -} - -type UpdateHomeURLParams struct { - HomeURL string `json:"home_url"` -} - -func (s *InstanceService) UpdateHomeURL(params UpdateHomeURLParams) error { - req, _ := s.client.NewRequest(http.MethodPost, "instance/change_domain", ¶ms) - - _, err := s.client.Do(req, nil) - if err != nil { - return err - } - return nil -} diff --git a/clerk/instances_test.go b/clerk/instances_test.go deleted file mode 100644 index 99a80af7..00000000 --- a/clerk/instances_test.go +++ /dev/null @@ -1,151 +0,0 @@ -package clerk - -import ( - "encoding/json" - "fmt" - "net/http" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestInstanceService_Update_happyPath(t *testing.T) { - token := "token" - client, mux, _, teardown := setup(token) - defer teardown() - - mux.HandleFunc("/instance", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, http.MethodPatch) - testHeader(t, req, "Authorization", "Bearer "+token) - w.WriteHeader(http.StatusNoContent) - }) - - enabled := true - supportEmail := "support@clerk.dev" - clerkJSVersion := "42" - err := client.Instances().Update(UpdateInstanceParams{ - TestMode: &enabled, - HIBP: &enabled, - EnhancedEmailDeliverability: &enabled, - SupportEmail: &supportEmail, - ClerkJSVersion: &clerkJSVersion, - }) - - if err != nil { - t.Errorf("expected no error to be returned, found %v instead", err) - } -} - -func TestInstanceService_Update_invalidServer(t *testing.T) { - client, _ := NewClient("token") - - enabled := true - supportEmail := "support@clerk.dev" - clerkJSVersion := "999" - err := client.Instances().Update(UpdateInstanceParams{ - TestMode: &enabled, - HIBP: &enabled, - EnhancedEmailDeliverability: &enabled, - SupportEmail: &supportEmail, - ClerkJSVersion: &clerkJSVersion, - }) - if err == nil { - t.Errorf("Expected error to be returned") - } -} - -func TestInstanceService_UpdateRestrictions_happyPath(t *testing.T) { - token := "token" - dummyRestrictionsResponseJSON := `{ - "allowlist": true, - "blocklist": true, - "block_email_subaddresses": true, - "block_disposable_email_domains": true - }` - var restrictionsResponse InstanceRestrictionsResponse - err := json.Unmarshal([]byte(dummyRestrictionsResponseJSON), &restrictionsResponse) - assert.NoError(t, err) - - client, mux, _, teardown := setup(token) - defer teardown() - - mux.HandleFunc("/instance/restrictions", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, http.MethodPatch) - testHeader(t, req, "Authorization", "Bearer "+token) - fmt.Fprint(w, dummyRestrictionsResponseJSON) - }) - - enabled := true - got, _ := client.Instances().UpdateRestrictions(UpdateRestrictionsParams{ - Allowlist: &enabled, - Blocklist: &enabled, - BlockEmailSubaddresses: &enabled, - BlockDisposableEmailDomains: &enabled, - }) - - assert.Equal(t, &restrictionsResponse, got) -} - -func TestInstanceService_UpdateRestrictions_invalidServer(t *testing.T) { - client, _ := NewClient("token") - - enabled := true - _, err := client.Instances().UpdateRestrictions(UpdateRestrictionsParams{ - Allowlist: &enabled, - Blocklist: &enabled, - }) - if err == nil { - t.Errorf("Expected error to be returned") - } -} - -func TestInstanceService_UpdateOrganizationSettings_happyPath(t *testing.T) { - token := "token" - dummyOrganizationSettingsResponseJSON := `{ - "enabled": true, - "max_allowed_memberships": 2, - "max_allowed_roles": 10, - "max_allowed_permissions": 50, - "creator_role": "org:custom_admin", - "admin_delete_enabled": true, - "domains_enabled": true, - "domains_enrollment_modes": [ - "manual_invitation", - "automatic_invitation", - "automatic_suggestion" - ], - "domains_default_role": "org:custom_domains" - }` - var organizationSettingsResponse OrganizationSettingsResponse - _ = json.Unmarshal([]byte(dummyOrganizationSettingsResponseJSON), &organizationSettingsResponse) - - client, mux, _, teardown := setup(token) - defer teardown() - - mux.HandleFunc("/instance/organization_settings", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, http.MethodPatch) - testHeader(t, req, "Authorization", "Bearer "+token) - fmt.Fprint(w, dummyOrganizationSettingsResponseJSON) - }) - - enabled := true - got, _ := client.Instances().UpdateOrganizationSettings(UpdateOrganizationSettingsParams{ - Enabled: &enabled, - CreatorRoleID: stringToPtr("role_2XcSZn6swGCjX59Nk0XbGer22jb"), - DomainsDefaultRoleID: stringToPtr("role_2XZCQwxfLbXOz2hoBXKFVRjwmGc"), - }) - - assert.Equal(t, &organizationSettingsResponse, got) -} - -func TestInstanceService_UpdateOrganizationSettings_invalidServer(t *testing.T) { - client, _ := NewClient("token") - - enabled := true - _, err := client.Instances().UpdateOrganizationSettings(UpdateOrganizationSettingsParams{ - Enabled: &enabled, - }) - if err == nil { - t.Errorf("Expected error to be returned") - } -} diff --git a/clerk/jwks.go b/clerk/jwks.go deleted file mode 100644 index f7dc1d40..00000000 --- a/clerk/jwks.go +++ /dev/null @@ -1,23 +0,0 @@ -package clerk - -import ( - "net/http" - - "github.com/go-jose/go-jose/v3" -) - -type JWKSService service - -type JWKS jose.JSONWebKeySet - -func (s *JWKSService) ListAll() (*JWKS, error) { - req, _ := s.client.NewRequest(http.MethodGet, "jwks", nil) - - jwks := JWKS{} - _, err := s.client.Do(req, &jwks) - if err != nil { - return nil, err - } - - return &jwks, nil -} diff --git a/clerk/jwks_cache.go b/clerk/jwks_cache.go deleted file mode 100644 index 04924b7f..00000000 --- a/clerk/jwks_cache.go +++ /dev/null @@ -1,43 +0,0 @@ -package clerk - -import ( - "fmt" - "sync" - "time" - - "github.com/go-jose/go-jose/v3" -) - -type jwksCache struct { - sync.RWMutex - jwks *JWKS - expiresAt time.Time -} - -func (c *jwksCache) isInvalid() bool { - c.RLock() - defer c.RUnlock() - - return c.jwks == nil || len(c.jwks.Keys) == 0 || time.Now().After(c.expiresAt) -} - -func (c *jwksCache) set(jwks *JWKS) { - c.Lock() - defer c.Unlock() - - c.jwks = jwks - c.expiresAt = time.Now().Add(time.Hour) -} - -func (c *jwksCache) get(kid string) (*jose.JSONWebKey, error) { - c.RLock() - defer c.RUnlock() - - for _, key := range c.jwks.Keys { - if key.KeyID == kid { - return &key, nil - } - } - - return nil, fmt.Errorf("no jwk key found for kid %s", kid) -} diff --git a/clerk/jwks_test.go b/clerk/jwks_test.go deleted file mode 100644 index d90f675a..00000000 --- a/clerk/jwks_test.go +++ /dev/null @@ -1,58 +0,0 @@ -package clerk - -import ( - "encoding/json" - "fmt" - "net/http" - "reflect" - "testing" -) - -func TestJWKSService_ListAll_happyPath(t *testing.T) { - c, mux, _, teardown := setup("token") - defer teardown() - - mux.HandleFunc("/jwks", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "GET") - testHeader(t, req, "Authorization", "Bearer token") - fmt.Fprint(w, dummyJWKSJson) - }) - - want := &JWKS{} - _ = json.Unmarshal([]byte(dummyJWKSJson), want) - - got, _ := c.JWKS().ListAll() - - if !reflect.DeepEqual(got, want) { - t.Errorf("Response = %v, want %v", got, want) - } -} - -func TestJWKSService_ListAll_invalidServer(t *testing.T) { - c, _ := NewClient("token") - - jwks, err := c.JWKS().ListAll() - if err == nil { - t.Errorf("Expected error to be returned") - } - - if jwks != nil { - t.Errorf("Was not expecting any jwks to be returned, instead got %+v", jwks) - } -} - -const dummyJWKSJson = ` -{ - "keys": - [ - { - "use":"sig", - "kty":"RSA", - "kid":"kid", - "alg":"RS256", - "n":"8ffrRMLd1z50B1hJcEfoxPac2wm9U_SXCnoXxSg5frRyW1oI1t9e78y8sOOwUt-IU4FXNcNK93dsCDQMeDBc6EfLxPBHuCB4SbVvsbpdMH8XSy9qLH6AJmS1GqOldYG0VkP1YzSwGXTkflgcDLCtYOHxkjiK6m5TnhJ4tu77bkjPrINiWAo4jAYBCjk1gqiW3LZWZwzwvqF_7n8g50JbhoTiJi2z6rd0anSFgi1A9AbViKwlzdxkll1uW90W1kn_Zs6lC6Yz7-X9WmelhxxUoLVE49BcCQ82PtmlBvxDQk7rREPLRbvzJSI0RIw1HMChRkZC_KtsLNkgPKq5tY_YSw", - "e":"AQAB" - } - ] - } -` diff --git a/clerk/jwt_templates.go b/clerk/jwt_templates.go deleted file mode 100644 index c450326a..00000000 --- a/clerk/jwt_templates.go +++ /dev/null @@ -1,143 +0,0 @@ -package clerk - -import ( - "encoding/json" - "fmt" - "net/http" -) - -type JWTTemplatesService service - -type JWTTemplate struct { - ID string `json:"id"` - Object string `json:"object"` - Name string `json:"name"` - Claims json.RawMessage `json:"claims"` - Lifetime int `json:"lifetime"` - AllowedClockSkew int `json:"allowed_clock_skew"` - CustomSigningKey bool `json:"custom_signing_key"` - SigningAlgorithm string `json:"signing_algorithm"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` -} - -type CreateUpdateJWTTemplate struct { - Name string `json:"name"` - Claims map[string]interface{} `json:"claims"` - Lifetime *int `json:"lifetime"` - AllowedClockSkew *int `json:"allowed_clock_skew"` - - CustomSigningKey bool `json:"custom_signing_key"` - SigningAlgorithm *string `json:"signing_algorithm"` - SigningKey *string `json:"signing_key"` -} - -func (t CreateUpdateJWTTemplate) toRequest() (*createUpdateJWTTemplateRequest, error) { - claimsBytes, err := json.Marshal(t.Claims) - if err != nil { - return nil, err - } - - return &createUpdateJWTTemplateRequest{ - Claims: string(claimsBytes), - Name: t.Name, - Lifetime: t.Lifetime, - AllowedClockSkew: t.AllowedClockSkew, - CustomSigningKey: t.CustomSigningKey, - SigningAlgorithm: t.SigningAlgorithm, - SigningKey: t.SigningKey, - }, nil -} - -type createUpdateJWTTemplateRequest struct { - Claims string `json:"claims"` - Name string `json:"name"` - Lifetime *int `json:"lifetime,omitempty"` - AllowedClockSkew *int `json:"allowed_clock_skew,omitempty"` - CustomSigningKey bool `json:"custom_signing_key"` - SigningAlgorithm *string `json:"signing_algorithm,omitempty"` - SigningKey *string `json:"signing_key,omitempty"` -} - -func (s JWTTemplatesService) ListAll() ([]JWTTemplate, error) { - req, err := s.client.NewRequest(http.MethodGet, JWTTemplatesUrl) - if err != nil { - return nil, err - } - - jwtTemplates := make([]JWTTemplate, 0) - if _, err = s.client.Do(req, &jwtTemplates); err != nil { - return nil, err - } - - return jwtTemplates, nil -} - -func (s JWTTemplatesService) Read(id string) (*JWTTemplate, error) { - reqURL := fmt.Sprintf("%s/%s", JWTTemplatesUrl, id) - req, err := s.client.NewRequest(http.MethodGet, reqURL) - if err != nil { - return nil, err - } - - jwtTemplate := &JWTTemplate{} - if _, err = s.client.Do(req, jwtTemplate); err != nil { - return nil, err - } - - return jwtTemplate, nil -} - -func (s JWTTemplatesService) Create(params *CreateUpdateJWTTemplate) (*JWTTemplate, error) { - reqBody, err := params.toRequest() - if err != nil { - return nil, err - } - - req, err := s.client.NewRequest(http.MethodPost, JWTTemplatesUrl, reqBody) - if err != nil { - return nil, err - } - - resp := JWTTemplate{} - if _, err = s.client.Do(req, &resp); err != nil { - return nil, err - } - - return &resp, nil -} - -func (s JWTTemplatesService) Update(id string, params *CreateUpdateJWTTemplate) (*JWTTemplate, error) { - reqURL := fmt.Sprintf("%s/%s", JWTTemplatesUrl, id) - - reqBody, err := params.toRequest() - if err != nil { - return nil, err - } - - req, err := s.client.NewRequest(http.MethodPatch, reqURL, reqBody) - if err != nil { - return nil, err - } - - resp := JWTTemplate{} - if _, err = s.client.Do(req, &resp); err != nil { - return nil, err - } - - return &resp, nil -} - -func (s JWTTemplatesService) Delete(id string) (*DeleteResponse, error) { - reqURL := fmt.Sprintf("%s/%s", JWTTemplatesUrl, id) - req, err := s.client.NewRequest(http.MethodDelete, reqURL) - if err != nil { - return nil, err - } - - resp := &DeleteResponse{} - if _, err = s.client.Do(req, resp); err != nil { - return nil, err - } - return resp, nil -} diff --git a/clerk/jwt_templates_test.go b/clerk/jwt_templates_test.go deleted file mode 100644 index 87d968df..00000000 --- a/clerk/jwt_templates_test.go +++ /dev/null @@ -1,235 +0,0 @@ -package clerk - -import ( - "encoding/json" - "fmt" - "net/http" - "reflect" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestJWTTemplatesService_ListAll(t *testing.T) { - c, mux, _, teardown := setup("token") - defer teardown() - - dummyResponse := "[" + dummyJWTTemplateJSON + "]" - - mux.HandleFunc("/jwt_templates", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, http.MethodGet) - testHeader(t, req, "Authorization", "Bearer token") - _, _ = fmt.Fprint(w, dummyResponse) - }) - - got, err := c.JWTTemplates().ListAll() - assert.Nil(t, err) - - expected := make([]JWTTemplate, 0) - _ = json.Unmarshal([]byte(dummyResponse), &expected) - - if !reflect.DeepEqual(got, expected) { - t.Errorf("Response = %v, want %v", got, expected) - } -} - -func TestJWTTemplatesService_Read(t *testing.T) { - dummyResponse := dummyTemplateJSON - - c, mux, _, teardown := setup("token") - defer teardown() - - url := fmt.Sprintf("/jwt_templates/%s", dummyJWTTemplateID) - - mux.HandleFunc(url, func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, http.MethodGet) - testHeader(t, req, "Authorization", "Bearer token") - _, _ = fmt.Fprint(w, dummyResponse) - }) - - got, err := c.JWTTemplates().Read(dummyJWTTemplateID) - assert.Nil(t, err) - - expected := JWTTemplate{} - _ = json.Unmarshal([]byte(dummyResponse), &expected) - - if !reflect.DeepEqual(*got, expected) { - t.Errorf("Response = %v, want %v", got, expected) - } -} - -func TestJWTTemplatesService_Create(t *testing.T) { - dummyResponse := dummyJWTTemplateJSON - - c, mux, _, teardown := setup("token") - defer teardown() - - mux.HandleFunc("/jwt_templates", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, http.MethodPost) - testHeader(t, req, "Authorization", "Bearer token") - _, _ = fmt.Fprint(w, dummyResponse) - }) - - newJWTTmpl := &CreateUpdateJWTTemplate{ - Name: "Testing", - Claims: map[string]interface{}{ - "name": "{{user.first_name}}", - "role": "tester", - }, - } - - got, err := c.JWTTemplates().Create(newJWTTmpl) - assert.Nil(t, err) - - expected := JWTTemplate{} - _ = json.Unmarshal([]byte(dummyResponse), &expected) - - if !reflect.DeepEqual(*got, expected) { - t.Errorf("Response = %v, want %v", got, expected) - } -} - -func TestJWTTemplatesService_CreateWithCustomSigningKey(t *testing.T) { - c, mux, _, teardown := setup("token") - defer teardown() - - customSigningAlgorithm := "HS256" - customSigningKey := "random-secret" - - mux.HandleFunc("/jwt_templates", func(w http.ResponseWriter, r *http.Request) { - testHttpMethod(t, r, http.MethodPost) - testHeader(t, r, "Authorization", "Bearer token") - - var req createUpdateJWTTemplateRequest - _ = json.NewDecoder(r.Body).Decode(&req) - - assert.Equal(t, true, req.CustomSigningKey) - assert.Equal(t, customSigningAlgorithm, *req.SigningAlgorithm) - assert.Equal(t, customSigningKey, *req.SigningKey) - - _, _ = fmt.Fprint(w, dummyJWTTemplateCustomSigningKeyJSON) - }) - - newJWTTmpl := &CreateUpdateJWTTemplate{ - Name: "Testing-Custom-Signing-Key", - Claims: map[string]interface{}{ - "name": "{{user.first_name}}", - "role": "tester", - }, - CustomSigningKey: true, - SigningAlgorithm: &customSigningAlgorithm, - SigningKey: &customSigningKey, - } - - _, err := c.JWTTemplates().Create(newJWTTmpl) - assert.Nil(t, err) -} - -func TestJWTTemplatesService_Update(t *testing.T) { - dummyResponse := dummyJWTTemplateCustomLifetimeAndClockSkewJSON - - c, mux, _, teardown := setup("token") - defer teardown() - - url := fmt.Sprintf("/jwt_templates/%s", dummyJWTTemplateID) - - mux.HandleFunc(url, func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, http.MethodPatch) - testHeader(t, req, "Authorization", "Bearer token") - _, _ = fmt.Fprint(w, dummyResponse) - }) - - updateJWTTmpl := &CreateUpdateJWTTemplate{ - Name: "New-Testing", - Claims: map[string]interface{}{ - "name": "{{user.first_name}}", - "age": 28, - }, - } - - got, err := c.JWTTemplates().Update(dummyJWTTemplateID, updateJWTTmpl) - assert.Nil(t, err) - - expected := JWTTemplate{} - _ = json.Unmarshal([]byte(dummyResponse), &expected) - - if !reflect.DeepEqual(*got, expected) { - t.Errorf("Response = %v, want %v", got, expected) - } -} - -func TestJWTTemplatesService_Delete(t *testing.T) { - c, mux, _, teardown := setup("token") - defer teardown() - - url := fmt.Sprintf("/jwt_templates/%s", dummyJWTTemplateID) - - mux.HandleFunc(url, func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, http.MethodDelete) - testHeader(t, req, "Authorization", "Bearer token") - response := fmt.Sprintf(`{ "deleted": true, "id": "%s", "object": "jwt_template" }`, dummyJWTTemplateID) - _, _ = fmt.Fprint(w, response) - }) - - expected := DeleteResponse{ - ID: dummyJWTTemplateID, - Object: "jwt_template", - Deleted: true, - } - - got, err := c.JWTTemplates().Delete(dummyJWTTemplateID) - assert.Nil(t, err) - - if !reflect.DeepEqual(*got, expected) { - t.Errorf("Response = %v, want %v", *got, expected) - } -} - -const ( - dummyJWTTemplateID = "jtmp_21xC2Ziqscwjq43MtC3CN6Pngbo" - - dummyJWTTemplateJSON = ` -{ - "object": "jwt_template", - "id": "` + dummyJWTTemplateID + `", - "name": "Testing", - "claims": { - "name": "{{user.first_name}}", - "role": "tester" - }, - "lifetime": 60, - "allowed_clock_skew": 5, - "custom_signing_key": false, - "signing_algorithm": "RS256" -}` - - dummyJWTTemplateCustomLifetimeAndClockSkewJSON = ` -{ - "object": "jwt_template", - "id": "` + dummyJWTTemplateID + `", - "name": "New-Testing", - "claims": { - "name": "{{user.first_name}}", - "age": 28 - }, - "lifetime": 60, - "allowed_clock_skew": 5, - "custom_signing_key": false, - "signing_algorithm": "RS256" -}` - - dummyJWTTemplateCustomSigningKeyJSON = ` -{ - "object": "jwt_template", - "id": "` + dummyJWTTemplateID + `", - "name": "Testing", - "claims": { - "name": "{{user.first_name}}", - "role": "tester" - }, - "lifetime": 60, - "allowed_clock_skew": 5, - "custom_signing_key": true, - "signing_algorithm": "HS256" -}` -) diff --git a/clerk/middleware.go b/clerk/middleware.go deleted file mode 100644 index 47ade1ae..00000000 --- a/clerk/middleware.go +++ /dev/null @@ -1,71 +0,0 @@ -package clerk - -import ( - "context" - "net/http" - "strings" -) - -const ( - ActiveSession = iota - ActiveSessionClaims - -// TODO: we should use a type alias instead of int, so as to avoid collisions -// with other packages -) - -// Deprecated: this middleware handles the old authentication scheme. Use -// WithSessionV2 instead. -func WithSession(client Client) func(handler http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if token, isAuthV2 := isAuthV2Request(r, client); isAuthV2 { - // Validate using session token - claims, err := client.VerifyToken(token) - if err != nil { - w.WriteHeader(http.StatusUnauthorized) - _, _ = w.Write([]byte(err.Error())) - return - } - - ctx := context.WithValue(r.Context(), ActiveSessionClaims, claims) - next.ServeHTTP(w, r.WithContext(ctx)) - } else { - // Validate using session verify request - session, err := client.Verification().Verify(r) - if err != nil { - w.WriteHeader(http.StatusBadRequest) - _, _ = w.Write([]byte(err.Error())) - return - } - - ctx := context.WithValue(r.Context(), ActiveSession, session) - next.ServeHTTP(w, r.WithContext(ctx)) - } - }) - } -} - -func isAuthV2Request(r *http.Request, client Client) (string, bool) { - // Try with token from header - headerToken := strings.TrimSpace(r.Header.Get("Authorization")) - headerToken = strings.TrimPrefix(headerToken, "Bearer ") - - claims, err := client.DecodeToken(headerToken) - if err == nil { - return headerToken, newIssuer(claims.Issuer).IsValid() - } - - // Verification from header token failed, try with token from cookie - cookieSession, err := r.Cookie(CookieSession) - if err != nil { - return "", false - } - - claims, err = client.DecodeToken(cookieSession.Value) - if err != nil { - return "", false - } - - return cookieSession.Value, newIssuer(claims.Issuer).IsValid() -} diff --git a/clerk/middleware_test.go b/clerk/middleware_test.go deleted file mode 100644 index fb1cc927..00000000 --- a/clerk/middleware_test.go +++ /dev/null @@ -1,213 +0,0 @@ -package clerk - -import ( - "encoding/json" - "fmt" - "net/http" - "reflect" - "testing" - "time" - - "github.com/go-jose/go-jose/v3" - "github.com/go-jose/go-jose/v3/jwt" -) - -func TestWithSession_addSessionToContext(t *testing.T) { - apiToken := "apiToken" - sessionId := "someSessionId" - sessionToken := "someSessionToken" - - client, mux, serverUrl, teardown := setup(apiToken) - defer teardown() - - expectedResponse := dummySessionJson - - mux.HandleFunc("/sessions/"+sessionId+"/verify", func(w http.ResponseWriter, req *http.Request) { - fmt.Fprint(w, expectedResponse) - }) - - dummyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // this handler should be called after the middleware has added the `ActiveSession` - activeSession := r.Context().Value(ActiveSession) - resp, _ := json.Marshal(activeSession) - fmt.Fprint(w, string(resp)) - }) - - mux.Handle("/session", WithSession(client)(dummyHandler)) - - request := setupRequest(&sessionId, &sessionToken) - request.URL.Host = serverUrl.Host - request.URL.Path = "/v1/session" - - var got Session - _, _ = client.Do(request, &got) - - var want Session - _ = json.Unmarshal([]byte(expectedResponse), &want) - - if !reflect.DeepEqual(got, want) { - t.Errorf("Response = %v, want %v", got, want) - } -} - -func TestWithSession_returnsErrorIfVerificationFails(t *testing.T) { - apiToken := "apiToken" - sessionId := "someSessionId" - sessionToken := "someSessionToken" - - client, mux, serverUrl, teardown := setup(apiToken) - defer teardown() - - mux.HandleFunc("/sessions/"+sessionId+"/verify", func(w http.ResponseWriter, req *http.Request) { - // return error - w.WriteHeader(400) - }) - - dummyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // this handler should be called after the middleware has added the `ActiveSession` - t.Errorf("This should never be called!") - }) - - mux.Handle("/session", WithSession(client)(dummyHandler)) - - request := setupRequest(&sessionId, &sessionToken) - request.URL.Host = serverUrl.Host - request.URL.Path = "/v1/session" - - resp, _ := client.Do(request, nil) - - if resp.StatusCode != http.StatusBadRequest { - t.Errorf("Was expecting 400 error code, found %v instead", resp.StatusCode) - } -} - -func TestWithSession_addSessionClaimsToContext_Header(t *testing.T) { - c, mux, serverUrl, teardown := setup("apiToken") - defer teardown() - - expectedClaims := dummySessionClaims - - token, pubKey := testGenerateTokenJWT(t, expectedClaims, "kid") - - client := c.(*client) - client.jwksCache.set(testBuildJWKS(t, pubKey, jose.RS256, "kid")) - - dummyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // this handler should be called after the middleware has added the `ActiveClaims` - claims := r.Context().Value(ActiveSessionClaims) - _ = json.NewEncoder(w).Encode(claims) - }) - - mux.Handle("/claims", WithSession(c)(dummyHandler)) - - req, _ := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/claims", serverUrl), nil) - req.Header.Set("Authorization", token) - - resp, err := http.DefaultClient.Do(req) - if err != nil { - t.Fatal(err) - } - - defer resp.Body.Close() - - var got SessionClaims - _ = json.NewDecoder(resp.Body).Decode(&got) - - if !reflect.DeepEqual(got, expectedClaims) { - t.Errorf("Response = %v, want %v", got, expectedClaims) - } -} - -func TestWithSession_addSessionClaimsToContext_Cookie(t *testing.T) { - c, mux, serverUrl, teardown := setup("apiToken") - defer teardown() - - expectedClaims := dummySessionClaims - - token, pubKey := testGenerateTokenJWT(t, expectedClaims, "kid") - - client := c.(*client) - client.jwksCache.set(testBuildJWKS(t, pubKey, jose.RS256, "kid")) - - dummyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // this handler should be called after the middleware has added the `ActiveClaims` - activeClaims := r.Context().Value(ActiveSessionClaims) - _ = json.NewEncoder(w).Encode(activeClaims) - }) - - mux.Handle("/claims", WithSession(c)(dummyHandler)) - - req, _ := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/claims", serverUrl), nil) - req.AddCookie(&http.Cookie{ - Name: "__session", - Value: token, - Secure: true, - HttpOnly: true, - }) - - resp, err := http.DefaultClient.Do(req) - if err != nil { - t.Fatal(err) - } - - defer resp.Body.Close() - - var got SessionClaims - _ = json.NewDecoder(resp.Body).Decode(&got) - - if !reflect.DeepEqual(got, expectedClaims) { - t.Errorf("Response = %v, want %v", got, expectedClaims) - } -} - -func TestWithSession_returnsErrorIfTokenVerificationFails(t *testing.T) { - c, mux, serverUrl, teardown := setup("apiToken") - defer teardown() - - expectedClaims := dummySessionClaims - expectedClaims.Expiry = jwt.NewNumericDate(time.Now().Add(time.Second * -1)) - - token, pubKey := testGenerateTokenJWT(t, expectedClaims, "kid") - - client := c.(*client) - client.jwksCache.set(testBuildJWKS(t, pubKey, jose.RS256, "kid")) - - dummyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // this handler should be called after the middleware has added the `ActiveClaims` - activeClaims := r.Context().Value(ActiveSessionClaims) - _ = json.NewEncoder(w).Encode(activeClaims) - }) - - mux.Handle("/claims", WithSession(c)(dummyHandler)) - - req, _ := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/claims", serverUrl), nil) - req.Header.Set("Authorization", token) - - resp, _ := http.DefaultClient.Do(req) - - if resp.StatusCode != http.StatusUnauthorized { - t.Errorf("Was expecting 401 error code, found %v instead", resp.StatusCode) - } -} - -func TestWithSession_returnsErrorIfTokenMissing(t *testing.T) { - apiToken := "apiToken" - - c, mux, serverUrl, teardown := setup(apiToken) - defer teardown() - - dummyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // this handler should be called after the middleware has added the `ActiveSession` - t.Errorf("This should never be called!") - }) - - mux.Handle("/claims", WithSession(c)(dummyHandler)) - - req, _ := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/claims", serverUrl), nil) - - resp, _ := http.DefaultClient.Do(req) - - if resp.StatusCode != http.StatusBadRequest { - t.Errorf("Was expecting 400 error code, found %v instead", resp.StatusCode) - } -} diff --git a/clerk/middleware_v2.go b/clerk/middleware_v2.go deleted file mode 100644 index 7569c2f2..00000000 --- a/clerk/middleware_v2.go +++ /dev/null @@ -1,200 +0,0 @@ -package clerk - -import ( - "context" - "errors" - "net" - "net/http" - "net/url" - "regexp" - "strconv" - "strings" - - "github.com/go-jose/go-jose/v3/jwt" -) - -var urlSchemeRe = regexp.MustCompile(`(^\w+:|^)\/\/`) - -// RequireSessionV2 will hijack the request and return an HTTP status 403 -// if the session is not authenticated. -func RequireSessionV2(client Client, verifyTokenOptions ...VerifyTokenOption) func(handler http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - f := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - claims, ok := r.Context().Value(ActiveSessionClaims).(*SessionClaims) - if !ok || claims == nil { - w.WriteHeader(http.StatusForbidden) - return - } - - next.ServeHTTP(w, r) - }) - - return WithSessionV2(client, verifyTokenOptions...)(f) - } -} - -// SessionFromContext returns the session's (if any) claims, as parsed from the -// token. -func SessionFromContext(ctx context.Context) (*SessionClaims, bool) { - c, ok := ctx.Value(ActiveSessionClaims).(*SessionClaims) - return c, ok -} - -// WithSessionV2 is the de-facto authentication middleware and should be -// preferred to WithSession. If the session is authenticated, it adds the corresponding -// session claims found in the JWT to request's context. -func WithSessionV2(client Client, verifyTokenOptions ...VerifyTokenOption) func(handler http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // **************************************************** - // * - // HEADER AUTHENTICATION * - // * - // **************************************************** - _, authorizationHeaderExists := r.Header["Authorization"] - - if authorizationHeaderExists { - headerToken := strings.TrimSpace(r.Header.Get("Authorization")) - headerToken = strings.TrimPrefix(headerToken, "Bearer ") - - _, err := client.DecodeToken(headerToken) - if err != nil { - // signed out - next.ServeHTTP(w, r) - return - } - - claims, err := client.VerifyToken(headerToken, verifyTokenOptions...) - if err == nil { // signed in - ctx := context.WithValue(r.Context(), ActiveSessionClaims, claims) - next.ServeHTTP(w, r.WithContext(ctx)) - return - } - - // Clerk.js should refresh the token and retry - w.WriteHeader(http.StatusUnauthorized) - return - } - - // In development or staging environments only, based on the request User Agent, detect non-browser - // requests (e.g. scripts). If there is no Authorization header, consider the user as signed out - // and prevent interstitial rendering - if isDevelopmentOrStaging(client) && !strings.HasPrefix(r.UserAgent(), "Mozilla/") { - // signed out - next.ServeHTTP(w, r) - return - } - - // in cross-origin requests the use of Authorization - // header is mandatory - if isCrossOrigin(r) { - // signed out - next.ServeHTTP(w, r) - return - } - - // **************************************************** - // * - // COOKIE AUTHENTICATION * - // * - // **************************************************** - cookieToken, _ := r.Cookie("__session") - clientUat, _ := r.Cookie("__client_uat") - - if isDevelopmentOrStaging(client) && (r.Referer() == "" || isCrossOrigin(r)) { - renderInterstitial(client, w) - return - } - - if isProduction(client) && clientUat == nil { - next.ServeHTTP(w, r) - return - } - - if clientUat != nil && clientUat.Value == "0" { - next.ServeHTTP(w, r) - return - } - - if clientUat == nil { - renderInterstitial(client, w) - return - } - - var clientUatTs int64 - ts, err := strconv.ParseInt(clientUat.Value, 10, 64) - if err == nil { - clientUatTs = ts - } - - if cookieToken == nil { - renderInterstitial(client, w) - return - } - - claims, err := client.VerifyToken(cookieToken.Value, verifyTokenOptions...) - - if err == nil { - if claims.IssuedAt != nil && clientUatTs <= int64(*claims.IssuedAt) { - ctx := context.WithValue(r.Context(), ActiveSessionClaims, claims) - next.ServeHTTP(w, r.WithContext(ctx)) - return - } - - renderInterstitial(client, w) - return - } - - if errors.Is(err, jwt.ErrExpired) || errors.Is(err, jwt.ErrIssuedInTheFuture) { - renderInterstitial(client, w) - return - } - - // signed out - next.ServeHTTP(w, r) - return - }) - } -} - -func isCrossOrigin(r *http.Request) bool { - // origin contains scheme+host and optionally port (ommitted if 80 or 443) - // ref. https://www.rfc-editor.org/rfc/rfc6454#section-6.1 - origin := strings.TrimSpace(r.Header.Get("Origin")) - origin = urlSchemeRe.ReplaceAllString(origin, "") // strip scheme - if origin == "" { - return false - } - - // parse request's host and port, taking into account reverse proxies - u := &url.URL{Host: r.Host} - host := strings.TrimSpace(r.Header.Get("X-Forwarded-Host")) - if host == "" { - host = u.Hostname() - } - port := strings.TrimSpace(r.Header.Get("X-Forwarded-Port")) - if port == "" { - port = u.Port() - } - - if port != "" && port != "80" && port != "443" { - host = net.JoinHostPort(host, port) - } - - return origin != host -} - -func isDevelopmentOrStaging(c Client) bool { - return strings.HasPrefix(c.APIKey(), "test_") || strings.HasPrefix(c.APIKey(), "sk_test_") -} - -func isProduction(c Client) bool { - return !isDevelopmentOrStaging(c) -} - -func renderInterstitial(c Client, w http.ResponseWriter) { - w.Header().Set("content-type", "text/html") - w.WriteHeader(401) - resp, _ := c.Interstitial() - w.Write(resp) -} diff --git a/clerk/middleware_v2_test.go b/clerk/middleware_v2_test.go deleted file mode 100644 index dc19db25..00000000 --- a/clerk/middleware_v2_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package clerk - -import ( - "fmt" - "net/http" - "testing" -) - -func TestWithSessionV2_nonBrowserRequest(t *testing.T) { - c, mux, serverUrl, teardown := setup("test_dummy") - defer teardown() - - dummyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Should be signed out - _, ok := r.Context().Value(ActiveSessionClaims).(*SessionClaims) - if ok { - t.Error("Expected session claims not to be present in request context") - } - }) - - mux.Handle("/dummy", WithSessionV2(c)(dummyHandler)) - - req, _ := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/dummy", serverUrl), nil) - req.Header.Set("User-Agent", "curl/7.64.1") - - _, err := http.DefaultClient.Do(req) - if err != nil { - t.Fatal(err) - } -} - -func TestWithSessionV2_emptyAuthorizationHeader(t *testing.T) { - c, mux, serverUrl, teardown := setup("test_dummy") - defer teardown() - - dummyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Should be signed out - _, ok := r.Context().Value(ActiveSessionClaims).(*SessionClaims) - if ok { - t.Error("Expected session claims not to be present in request context") - } - }) - - mux.Handle("/dummy", WithSessionV2(c)(dummyHandler)) - - req, _ := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/dummy", serverUrl), nil) - req.Header.Set("Authorization", "") - - _, err := http.DefaultClient.Do(req) - if err != nil { - t.Fatal(err) - } -} diff --git a/clerk/organizations.go b/clerk/organizations.go deleted file mode 100644 index db7bf2e4..00000000 --- a/clerk/organizations.go +++ /dev/null @@ -1,387 +0,0 @@ -package clerk - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/http" - "strconv" -) - -type OrganizationsService service - -type Organization struct { - Object string `json:"object"` - ID string `json:"id"` - Name string `json:"name"` - Slug *string `json:"slug"` - LogoURL *string `json:"logo_url"` - ImageURL *string `json:"image_url,omitempty"` - HasImage bool `json:"has_image"` - MembersCount *int `json:"members_count,omitempty"` - MaxAllowedMemberships int `json:"max_allowed_memberships"` - AdminDeleteEnabled bool `json:"admin_delete_enabled"` - PublicMetadata json.RawMessage `json:"public_metadata"` - PrivateMetadata json.RawMessage `json:"private_metadata,omitempty"` - CreatedBy string `json:"created_by"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` -} - -type CreateOrganizationParams struct { - Name string `json:"name"` - Slug *string `json:"slug,omitempty"` - CreatedBy string `json:"created_by"` - MaxAllowedMemberships *int `json:"max_allowed_memberships,omitempty"` - PublicMetadata json.RawMessage `json:"public_metadata,omitempty"` - PrivateMetadata json.RawMessage `json:"private_metadata,omitempty"` -} - -func (s *OrganizationsService) Create(params CreateOrganizationParams) (*Organization, error) { - req, _ := s.client.NewRequest(http.MethodPost, OrganizationsUrl, ¶ms) - - var organization Organization - _, err := s.client.Do(req, &organization) - if err != nil { - return nil, err - } - return &organization, nil -} - -type UpdateOrganizationParams struct { - Name *string `json:"name,omitempty"` - Slug *string `json:"slug,omitempty"` - MaxAllowedMemberships *int `json:"max_allowed_memberships,omitempty"` - AdminDeleteEnabled *bool `json:"admin_delete_enabled,omitempty"` - PublicMetadata json.RawMessage `json:"public_metadata,omitempty"` - PrivateMetadata json.RawMessage `json:"private_metadata,omitempty"` -} - -func (s *OrganizationsService) Update(organizationID string, params UpdateOrganizationParams) (*Organization, error) { - req, _ := s.client.NewRequest(http.MethodPatch, fmt.Sprintf("%s/%s", OrganizationsUrl, organizationID), ¶ms) - - var organization Organization - _, err := s.client.Do(req, &organization) - if err != nil { - return nil, err - } - return &organization, nil -} - -type UpdateOrganizationLogoParams struct { - File multipart.File - UploaderUserID string - Filename *string -} - -func (s *OrganizationsService) UpdateLogo(organizationID string, params UpdateOrganizationLogoParams) (*Organization, error) { - var buf bytes.Buffer - w := multipart.NewWriter(&buf) - uploaderUserID, err := w.CreateFormField("uploader_user_id") - if err != nil { - return nil, err - } - uploaderUserID.Write([]byte(params.UploaderUserID)) - - filename := "file" - if params.Filename != nil { - filename = *params.Filename - } - file, err := w.CreateFormFile("file", filename) - if err != nil { - return nil, err - } - defer params.File.Close() - _, err = io.Copy(file, params.File) - if err != nil { - return nil, err - } - w.Close() - - req, err := s.client.NewRequest(http.MethodPut, fmt.Sprintf("%s/%s/logo", OrganizationsUrl, organizationID)) - if err != nil { - return nil, err - } - req.Body = io.NopCloser(&buf) - req.Header.Set("content-type", w.FormDataContentType()) - - var organization Organization - _, err = s.client.Do(req, &organization) - return &organization, err -} - -func (s *OrganizationsService) DeleteLogo(organizationID string) (*Organization, error) { - req, _ := s.client.NewRequest(http.MethodDelete, fmt.Sprintf("%s/%s/logo", OrganizationsUrl, organizationID)) - var organization Organization - _, err := s.client.Do(req, &organization) - return &organization, err -} - -type UpdateOrganizationMetadataParams struct { - PublicMetadata json.RawMessage `json:"public_metadata,omitempty"` - PrivateMetadata json.RawMessage `json:"private_metadata,omitempty"` -} - -func (s *OrganizationsService) UpdateMetadata(organizationID string, params UpdateOrganizationMetadataParams) (*Organization, error) { - req, _ := s.client.NewRequest(http.MethodPatch, fmt.Sprintf("%s/%s/metadata", OrganizationsUrl, organizationID), ¶ms) - - var organization Organization - _, err := s.client.Do(req, &organization) - return &organization, err -} - -func (s *OrganizationsService) Delete(organizationID string) (*DeleteResponse, error) { - req, _ := s.client.NewRequest(http.MethodDelete, fmt.Sprintf("%s/%s", OrganizationsUrl, organizationID)) - - var deleteResponse DeleteResponse - _, err := s.client.Do(req, &deleteResponse) - if err != nil { - return nil, err - } - return &deleteResponse, nil -} - -func (s *OrganizationsService) Read(organizationIDOrSlug string) (*Organization, error) { - req, err := s.client.NewRequest(http.MethodGet, fmt.Sprintf("%s/%s", OrganizationsUrl, organizationIDOrSlug)) - if err != nil { - return nil, err - } - - var organization Organization - _, err = s.client.Do(req, &organization) - if err != nil { - return nil, err - } - return &organization, nil -} - -type OrganizationsResponse struct { - Data []Organization `json:"data"` - TotalCount int64 `json:"total_count"` -} - -type ListAllOrganizationsParams struct { - Limit *int - Offset *int - IncludeMembersCount bool - Query string - UserIDs []string - OrderBy *string -} - -func (s *OrganizationsService) ListAll(params ListAllOrganizationsParams) (*OrganizationsResponse, error) { - req, _ := s.client.NewRequest(http.MethodGet, OrganizationsUrl) - - query := req.URL.Query() - if params.Limit != nil { - query.Set("limit", strconv.Itoa(*params.Limit)) - } - if params.Offset != nil { - query.Set("offset", strconv.Itoa(*params.Offset)) - } - if params.IncludeMembersCount { - query.Set("include_members_count", strconv.FormatBool(params.IncludeMembersCount)) - } - if params.Query != "" { - query.Add("query", params.Query) - } - if params.OrderBy != nil { - query.Add("order_by", *params.OrderBy) - } - for _, userID := range params.UserIDs { - query.Add("user_id", userID) - } - req.URL.RawQuery = query.Encode() - - var organizationsResponse *OrganizationsResponse - _, err := s.client.Do(req, &organizationsResponse) - if err != nil { - return nil, err - } - return organizationsResponse, nil -} - -type OrganizationInvitation struct { - Object string `json:"object"` - ID string `json:"id"` - EmailAddress string `json:"email_address"` - OrganizationID string `json:"organization_id"` - PublicMetadata json.RawMessage `json:"public_metadata"` - Role string `json:"role"` - Status string `json:"status"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` -} - -type CreateOrganizationInvitationParams struct { - EmailAddress string `json:"email_address"` - InviterUserID string `json:"inviter_user_id"` - OrganizationID string `json:"organization_id"` - PublicMetadata json.RawMessage `json:"public_metadata,omitempty"` - RedirectURL string `json:"redirect_url,omitempty"` - Role string `json:"role"` -} - -func (s *OrganizationsService) CreateInvitation(params CreateOrganizationInvitationParams) (*OrganizationInvitation, error) { - endpoint := fmt.Sprintf("%s/%s/%s", OrganizationsUrl, params.OrganizationID, InvitationsURL) - req, _ := s.client.NewRequest(http.MethodPost, endpoint, ¶ms) - var organizationInvitation OrganizationInvitation - _, err := s.client.Do(req, &organizationInvitation) - return &organizationInvitation, err -} - -type ListOrganizationMembershipsParams struct { - OrganizationID string - Limit *int - Offset *int - Roles []string `json:"role"` - UserIDs []string `json:"user_id"` - EmailAddresses []string `json:"email_address"` - PhoneNumbers []string `json:"phone_number"` - Usernames []string `json:"username"` - Web3Wallets []string `json:"web3_wallet"` - OrderBy *string `json:"order_by"` - Query *string `json:"query"` -} - -type ΟrganizationMembershipPublicUserData struct { - FirstName *string `json:"first_name"` - LastName *string `json:"last_name"` - ProfileImageURL string `json:"profile_image_url"` - ImageURL *string `json:"image_url,omitempty"` - HasImage bool `json:"has_image"` - Identifier string `json:"identifier"` - UserID string `json:"user_id"` -} - -type OrganizationMembership struct { - Object string `json:"object"` - ID string `json:"id"` - PublicMetadata json.RawMessage `json:"public_metadata"` - PrivateMetadata json.RawMessage `json:"private_metadata"` - Role string `json:"role"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` - - Organization *Organization `json:"organization"` - PublicUserData *ΟrganizationMembershipPublicUserData `json:"public_user_data"` -} - -type ListOrganizationMembershipsResponse struct { - Data []OrganizationMembership `json:"data"` - TotalCount int64 `json:"total_count"` -} - -func (s *OrganizationsService) addMembersSearchParamsToRequest(r *http.Request, params ListOrganizationMembershipsParams) { - query := r.URL.Query() - for _, email := range params.EmailAddresses { - query.Add("email_address", email) - } - - for _, phone := range params.PhoneNumbers { - query.Add("phone_number", phone) - } - - for _, web3Wallet := range params.Web3Wallets { - query.Add("web3_wallet", web3Wallet) - } - - for _, username := range params.Usernames { - query.Add("username", username) - } - - for _, userID := range params.UserIDs { - query.Add("user_id", userID) - } - - for _, role := range params.Roles { - query.Add("role", role) - } - - if params.Query != nil { - query.Add("query", *params.Query) - } - - if params.OrderBy != nil { - query.Add("order_by", *params.OrderBy) - } - - r.URL.RawQuery = query.Encode() -} - -func (s *OrganizationsService) ListMemberships(params ListOrganizationMembershipsParams) (*ListOrganizationMembershipsResponse, error) { - endpoint := fmt.Sprintf("%s/%s/memberships", OrganizationsUrl, params.OrganizationID) - req, _ := s.client.NewRequest(http.MethodGet, endpoint) - - s.addMembersSearchParamsToRequest(req, ListOrganizationMembershipsParams{ - EmailAddresses: params.EmailAddresses, - PhoneNumbers: params.PhoneNumbers, - Web3Wallets: params.Web3Wallets, - Usernames: params.Usernames, - UserIDs: params.UserIDs, - Roles: params.Roles, - Query: params.Query, - OrderBy: params.OrderBy, - }) - - query := req.URL.Query() - if params.Limit != nil { - query.Set("limit", strconv.Itoa(*params.Limit)) - } - if params.Offset != nil { - query.Set("offset", strconv.Itoa(*params.Offset)) - } - req.URL.RawQuery = query.Encode() - - var membershipsResponse *ListOrganizationMembershipsResponse - _, err := s.client.Do(req, &membershipsResponse) - if err != nil { - return nil, err - } - return membershipsResponse, nil -} - -type CreateOrganizationMembershipParams struct { - UserID string `json:"user_id"` - Role string `json:"role"` -} - -func (s *OrganizationsService) CreateMembership(organizationID string, params CreateOrganizationMembershipParams) (*OrganizationMembership, error) { - req, _ := s.client.NewRequest(http.MethodPost, fmt.Sprintf("%s/%s/memberships", OrganizationsUrl, organizationID), ¶ms) - - var organizationMembership OrganizationMembership - _, err := s.client.Do(req, &organizationMembership) - if err != nil { - return nil, err - } - return &organizationMembership, nil -} - -type UpdateOrganizationMembershipParams struct { - UserID string `json:"user_id"` - Role string `json:"role"` -} - -func (s *OrganizationsService) UpdateMembership(organizationID string, params UpdateOrganizationMembershipParams) (*OrganizationMembership, error) { - req, _ := s.client.NewRequest(http.MethodPatch, fmt.Sprintf("%s/%s/memberships/%s", OrganizationsUrl, organizationID, params.UserID), ¶ms) - - var organizationMembership OrganizationMembership - _, err := s.client.Do(req, &organizationMembership) - if err != nil { - return nil, err - } - return &organizationMembership, nil -} - -func (s *OrganizationsService) DeleteMembership(organizationID, userID string) (*OrganizationMembership, error) { - req, _ := s.client.NewRequest(http.MethodDelete, fmt.Sprintf("%s/%s/memberships/%s", OrganizationsUrl, organizationID, userID)) - - var organizationMembership OrganizationMembership - _, err := s.client.Do(req, &organizationMembership) - if err != nil { - return nil, err - } - return &organizationMembership, nil -} diff --git a/clerk/organizations_test.go b/clerk/organizations_test.go deleted file mode 100644 index 29ef6a53..00000000 --- a/clerk/organizations_test.go +++ /dev/null @@ -1,343 +0,0 @@ -package clerk - -import ( - "bytes" - "encoding/json" - "fmt" - "net/http" - "net/url" - "os" - "path" - "reflect" - "strings" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestOrganizationsService_Read(t *testing.T) { - client, mux, _, teardown := setup("token") - defer teardown() - - expectedResponse := dummyOrganizationJson - orgID := "randomIDorSlug" - - mux.HandleFunc(fmt.Sprintf("/organizations/%s", orgID), func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "GET") - testHeader(t, req, "Authorization", "Bearer token") - fmt.Fprint(w, expectedResponse) - }) - - got, err := client.Organizations().Read(orgID) - if err != nil { - t.Fatal(err) - } - - var want Organization - err = json.Unmarshal([]byte(expectedResponse), &want) - if err != nil { - t.Fatal(err) - } - - if !reflect.DeepEqual(got, &want) { - t.Errorf("Response = %v, want %v", got, &want) - } -} - -func TestOrganizationsService_Update(t *testing.T) { - client, mux, _, teardown := setup("token") - defer teardown() - var payload UpdateOrganizationParams - _ = json.Unmarshal([]byte(dummyUpdateOrganizationJson), &payload) - - expectedResponse := dummyOrganizationJson - orgID := "randomIDorSlug" - - mux.HandleFunc(fmt.Sprintf("/organizations/%s", orgID), func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "PATCH") - testHeader(t, req, "Authorization", "Bearer token") - fmt.Fprint(w, expectedResponse) - }) - - got, err := client.Organizations().Update(orgID, payload) - if err != nil { - t.Fatal(err) - } - - var want Organization - err = json.Unmarshal([]byte(expectedResponse), &want) - if err != nil { - t.Fatal(err) - } - - if !reflect.DeepEqual(got, &want) { - t.Errorf("Response = %v, want %v", got, &want) - } -} - -func TestOrganizationsService_invalidServer(t *testing.T) { - client, _ := NewClient("token") - var payload UpdateOrganizationParams - _ = json.Unmarshal([]byte(dummyUpdateOrganizationJson), &payload) - - _, err := client.Organizations().Update("someOrgId", payload) - if err == nil { - t.Errorf("Expected error to be returned") - } -} - -func TestOrganizationsService_ListAll_happyPath(t *testing.T) { - client, mux, _, teardown := setup("token") - defer teardown() - - expectedResponse := fmt.Sprintf(`{ - "data": [%s], - "total_count": 1 - }`, dummyOrganizationJson) - - mux.HandleFunc("/organizations", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "GET") - testHeader(t, req, "Authorization", "Bearer token") - fmt.Fprint(w, expectedResponse) - }) - - var want *OrganizationsResponse - _ = json.Unmarshal([]byte(expectedResponse), &want) - - got, _ := client.Organizations().ListAll(ListAllOrganizationsParams{}) - if len(got.Data) != len(want.Data) { - t.Errorf("Was expecting %d organizations to be returned, instead got %d", len(want.Data), len(got.Data)) - } - - if !reflect.DeepEqual(got, want) { - t.Errorf("Response = %v, want %v", got, want) - } -} - -func TestOrganizationsService_ListAll_happyPathWithParameters(t *testing.T) { - client, mux, _, teardown := setup("token") - defer teardown() - - expectedResponse := fmt.Sprintf(`{ - "data": [%s], - "total_count": 1 - }`, dummyOrganizationJson) - - mux.HandleFunc("/organizations", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "GET") - testHeader(t, req, "Authorization", "Bearer token") - - actualQuery := req.URL.Query() - expectedQuery := url.Values(map[string][]string{ - "limit": {"5"}, - "offset": {"6"}, - "include_members_count": {"true"}, - }) - assert.Equal(t, expectedQuery, actualQuery) - fmt.Fprint(w, expectedResponse) - }) - - var want *OrganizationsResponse - _ = json.Unmarshal([]byte(expectedResponse), &want) - - limit := 5 - offset := 6 - got, _ := client.Organizations().ListAll(ListAllOrganizationsParams{ - Limit: &limit, - Offset: &offset, - IncludeMembersCount: true, - }) - if len(got.Data) != len(want.Data) { - t.Errorf("Was expecting %d organizations to be returned, instead got %d", len(want.Data), len(got.Data)) - } - - if !reflect.DeepEqual(got, want) { - t.Errorf("Response = %v, want %v", got, want) - } -} - -func TestOrganizationsService_ListAll_happyPathWithQuery(t *testing.T) { - client, mux, _, teardown := setup("token") - defer teardown() - - expectedResponse := fmt.Sprintf(`{ - "data": [%s], - "total_count": 1 - }`, dummyOrganizationJson) - - mux.HandleFunc("/organizations", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "GET") - testHeader(t, req, "Authorization", "Bearer token") - - actualQuery := req.URL.Query() - expectedQuery := url.Values(map[string][]string{ - "query": {"test"}, - }) - assert.Equal(t, expectedQuery, actualQuery) - fmt.Fprint(w, expectedResponse) - }) - - var want *OrganizationsResponse - _ = json.Unmarshal([]byte(expectedResponse), &want) - got, _ := client.Organizations().ListAll(ListAllOrganizationsParams{ - Query: "test", - }) - if len(got.Data) != len(want.Data) { - t.Errorf("Was expecting %d organizations to be returned, instead got %d", len(want.Data), len(got.Data)) - } - - if !reflect.DeepEqual(got, want) { - t.Errorf("Response = %v, want %v", got, want) - } -} - -func TestOrganizationsService_ListAll_invalidServer(t *testing.T) { - client, _ := NewClient("token") - - organizations, err := client.Organizations().ListAll(ListAllOrganizationsParams{}) - if err == nil { - t.Errorf("Expected error to be returned") - } - if organizations != nil { - t.Errorf("Was not expecting any organizations to be returned, instead got %v", organizations) - } -} - -func TestOrganizationsService_UpdateLogo(t *testing.T) { - client, mux, _, teardown := setup("token") - defer teardown() - - organizationID := "org_123" - uploaderUserID := "user_123" - expectedResponse := fmt.Sprintf(`{"id":"%s"}`, organizationID) - filename := "200x200-grayscale.jpg" - file, err := os.Open(path.Join("..", "testdata", filename)) - if err != nil { - t.Fatal(err) - } - defer file.Close() - - mux.HandleFunc( - fmt.Sprintf("/organizations/%s/logo", organizationID), - func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, http.MethodPut) - testHeader(t, req, "Authorization", "Bearer token") - // Assert that the request is sent as multipart/form-data - if !strings.Contains(req.Header["Content-Type"][0], "multipart/form-data") { - t.Errorf("expected content-type to be multipart/form-data, got %s", req.Header["Content-Type"]) - } - defer req.Body.Close() - - // Check that the file is sent correctly - fileParam, header, err := req.FormFile("file") - if err != nil { - t.Fatal(err) - } - if header.Filename != filename { - t.Errorf("expected %s, got %s", filename, header.Filename) - } - defer fileParam.Close() - - got := make([]byte, header.Size) - gotSize, err := fileParam.Read(got) - if err != nil { - t.Fatal(err) - } - fileInfo, err := file.Stat() - if err != nil { - t.Fatal(err) - } - want := make([]byte, fileInfo.Size()) - _, err = file.Seek(0, 0) - if err != nil { - t.Fatal(err) - } - wantSize, err := file.Read(want) - if err != nil { - t.Fatal(err) - } - if gotSize != wantSize { - t.Errorf("read different size of files") - } - if !bytes.Equal(got, want) { - t.Errorf("file was not sent correctly") - } - - // Check the uploader user ID - if got, ok := req.MultipartForm.Value["uploader_user_id"]; !ok || got[0] != uploaderUserID { - t.Errorf("expected %s, got %s", uploaderUserID, got) - } - - fmt.Fprint(w, expectedResponse) - }, - ) - - // Trigger a request to update the logo with the file - org, err := client.Organizations().UpdateLogo(organizationID, UpdateOrganizationLogoParams{ - File: file, - Filename: &filename, - UploaderUserID: "user_123", - }) - if err != nil { - t.Fatal(err) - } - if org.ID != organizationID { - t.Errorf("expected %s, got %s", organizationID, org.ID) - } -} - -func TestOrganizationsService_DeleteLogo(t *testing.T) { - client, mux, _, teardown := setup("token") - defer teardown() - - organizationID := "org_123" - mux.HandleFunc( - fmt.Sprintf("/organizations/%s/logo", organizationID), - func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, http.MethodDelete) - testHeader(t, req, "Authorization", "Bearer token") - fmt.Fprint(w, fmt.Sprintf(`{"id":"%s"}`, organizationID)) - }, - ) - - // Trigger a request to delete the logo - _, err := client.Organizations().DeleteLogo(organizationID) - if err != nil { - t.Fatal(err) - } -} - -const dummyOrganizationJson = `{ - "object": "organization", - "id": "org_1mebQggrD3xO5JfuHk7clQ94ysA", - "name": "test-org", - "slug": "org_slug", - "members_count": 42, - "created_by": "user_1mebQggrD3xO5JfuHk7clQ94ysA", - "created_at": 1610783813, - "updated_at": 1610783813, - "public_metadata": { - "address": { - "street": "Pennsylvania Avenue", - "number": "1600" - } - }, - "private_metadata": { - "app_id": 5 - } -}` - -const dummyUpdateOrganizationJson = `{ - "object": "organization", - "id": "org_1mebQggrD3xO5JfuHk7clQ94ysA", - "name": "test-org", - "slug": "org_slug", - "members_count": 42, - "created_by": "user_1mebQggrD3xO5JfuHk7clQ94ysA", - "created_at": 1610783813, - "updated_at": 1610783813, - "public_metadata": {}, - "private_metadata": { - "app_id": 8, - } -}` diff --git a/clerk/phone_numbers.go b/clerk/phone_numbers.go deleted file mode 100644 index ee297e2e..00000000 --- a/clerk/phone_numbers.go +++ /dev/null @@ -1,75 +0,0 @@ -package clerk - -import "fmt" - -type PhoneNumbersService service - -type PhoneNumber struct { - ID string `json:"id"` - Object string `json:"object"` - PhoneNumber string `json:"phone_number"` - ReservedForSecondFactor bool `json:"reserved_for_second_factor"` - DefaultSecondFactor bool `json:"default_second_factor"` - Reserved bool `json:"reserved"` - Verification *Verification `json:"verification"` - LinkedTo []IdentificationLink `json:"linked_to"` - BackupCodes []string `json:"backup_codes"` -} - -type CreatePhoneNumberParams struct { - UserID string `json:"user_id"` - PhoneNumber string `json:"phone_number"` - Verified *bool `json:"verified"` - Primary *bool `json:"primary"` -} - -type UpdatePhoneNumberParams struct { - Verified *bool `json:"verified"` - Primary *bool `json:"primary"` -} - -func (s *PhoneNumbersService) Create(params CreatePhoneNumberParams) (*PhoneNumber, error) { - req, _ := s.client.NewRequest("POST", PhoneNumbersURL, ¶ms) - - var response PhoneNumber - _, err := s.client.Do(req, &response) - if err != nil { - return nil, err - } - return &response, nil -} - -func (s *PhoneNumbersService) Read(phoneNumberID string) (*PhoneNumber, error) { - phoneNumberURL := fmt.Sprintf("%s/%s", PhoneNumbersURL, phoneNumberID) - req, _ := s.client.NewRequest("GET", phoneNumberURL) - - var phoneNumber PhoneNumber - _, err := s.client.Do(req, &phoneNumber) - if err != nil { - return nil, err - } - return &phoneNumber, nil -} - -func (s *PhoneNumbersService) Update(phoneNumberID string, params UpdatePhoneNumberParams) (*PhoneNumber, error) { - phoneNumberURL := fmt.Sprintf("%s/%s", PhoneNumbersURL, phoneNumberID) - req, _ := s.client.NewRequest("PATCH", phoneNumberURL, ¶ms) - - var phoneNumber PhoneNumber - _, err := s.client.Do(req, &phoneNumber) - if err != nil { - return nil, err - } - return &phoneNumber, nil -} - -func (s *PhoneNumbersService) Delete(phoneNumberID string) (*DeleteResponse, error) { - phoneNumberURL := fmt.Sprintf("%s/%s", PhoneNumbersURL, phoneNumberID) - req, _ := s.client.NewRequest("DELETE", phoneNumberURL) - - var delResponse DeleteResponse - if _, err := s.client.Do(req, &delResponse); err != nil { - return nil, err - } - return &delResponse, nil -} diff --git a/clerk/phone_numbers_test.go b/clerk/phone_numbers_test.go deleted file mode 100644 index 56ba8fe9..00000000 --- a/clerk/phone_numbers_test.go +++ /dev/null @@ -1,160 +0,0 @@ -package clerk - -import ( - "encoding/json" - "fmt" - "github.com/stretchr/testify/assert" - "net/http" - "reflect" - "testing" -) - -func TestPhoneNumbersService_Create_HappyPath(t *testing.T) { - token := "token" - expectedResponse := unverifiedPhoneNumberJSON - - verified := false - primary := false - - payload := CreatePhoneNumberParams{ - UserID: "user_abcdefg", - PhoneNumber: "+15555555555", - Verified: &verified, - Primary: &primary, - } - - client, mux, _, teardown := setup(token) - defer teardown() - - url := "/phone_numbers" - - mux.HandleFunc(url, func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "POST") - testHeader(t, req, "Authorization", "Bearer "+token) - fmt.Fprint(w, expectedResponse) - }) - - var want PhoneNumber - _ = json.Unmarshal([]byte(expectedResponse), &want) - - got, err := client.PhoneNumbers().Create(payload) - assert.Nil(t, err) - - if !reflect.DeepEqual(*got, want) { - t.Errorf("Response = %v, want %v", got, want) - } -} - -func TestPhoneNumbersService_Read_HappyPath(t *testing.T) { - token := "token" - phoneNumberID := "idn_banana" - expectedResponse := unverifiedPhoneNumberJSON - - client, mux, _, teardown := setup(token) - defer teardown() - - url := fmt.Sprintf("/phone_numbers/%s", phoneNumberID) - - mux.HandleFunc(url, func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "GET") - testHeader(t, req, "Authorization", "Bearer "+token) - fmt.Fprint(w, expectedResponse) - }) - - var want PhoneNumber - _ = json.Unmarshal([]byte(expectedResponse), &want) - - got, err := client.PhoneNumbers().Read(phoneNumberID) - assert.Nil(t, err) - - if !reflect.DeepEqual(*got, want) { - t.Errorf("Response = %v, want %v", got, want) - } -} - -func TestPhoneNumbersService_Update_HappyPath(t *testing.T) { - token := "token" - phoneNumberID := "idn_banana" - expectedResponse := verifiedPhoneNumberJSON - - verified := true - primary := true - - payload := UpdatePhoneNumberParams{ - Verified: &verified, - Primary: &primary, - } - - client, mux, _, teardown := setup(token) - defer teardown() - - url := fmt.Sprintf("/phone_numbers/%s", phoneNumberID) - - mux.HandleFunc(url, func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "PATCH") - testHeader(t, req, "Authorization", "Bearer "+token) - fmt.Fprint(w, expectedResponse) - }) - - var want PhoneNumber - _ = json.Unmarshal([]byte(expectedResponse), &want) - - got, err := client.PhoneNumbers().Update(phoneNumberID, payload) - assert.Nil(t, err) - - if !reflect.DeepEqual(*got, want) { - t.Errorf("Response = %v, want %v", got, want) - } -} - -func TestPhoneNumbersService_Delete_HappyPath(t *testing.T) { - token := "token" - phoneNumberID := "idn_banana" - - client, mux, _, teardown := setup(token) - defer teardown() - - url := fmt.Sprintf("/phone_numbers/%s", phoneNumberID) - - mux.HandleFunc(url, func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "DELETE") - testHeader(t, req, "Authorization", "Bearer "+token) - response := fmt.Sprintf(`{ "deleted": true, "id": "%v", "object": "phone_number" }`, phoneNumberID) - fmt.Fprint(w, response) - }) - - want := DeleteResponse{ID: phoneNumberID, Object: "phone_number", Deleted: true} - - got, _ := client.PhoneNumbers().Delete(phoneNumberID) - if !reflect.DeepEqual(*got, want) { - t.Errorf("Response = %v, want %v", *got, want) - } -} - -const unverifiedPhoneNumberJSON = `{ - "id": "idn_avocado", - "object": "phone_number", - "phone_number": "+15555555555", - "reserved_for_second_factor": false, - "default_second_factor": false, - "reserved": false, - "linked_to": [], - "backup_codes": null -}` - -const verifiedPhoneNumberJSON = `{ - "id": "idn_avocado", - "object": "phone_number", - "phone_number": "+15555555555", - "reserved_for_second_factor": false, - "default_second_factor": false, - "reserved": false, - "verification": { - "status": "verified", - "strategy": "admin", - "attempts": null, - "expire_at": null - }, - "linked_to": [], - "backup_codes": null -}` diff --git a/clerk/proxy_checks.go b/clerk/proxy_checks.go deleted file mode 100644 index 2202b615..00000000 --- a/clerk/proxy_checks.go +++ /dev/null @@ -1,31 +0,0 @@ -package clerk - -import "net/http" - -type ProxyChecksService service - -type ProxyCheck struct { - Object string `json:"object"` - ID string `json:"id"` - DomainID string `json:"domain_id"` - ProxyURL string `json:"proxy_url"` - Successful bool `json:"successful"` - LastRunAt *int64 `json:"last_run_at"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` -} - -type CreateProxyCheckParams struct { - DomainID string `json:"domain_id"` - ProxyURL string `json:"proxy_url"` -} - -func (s *ProxyChecksService) Create(params CreateProxyCheckParams) (*ProxyCheck, error) { - req, _ := s.client.NewRequest(http.MethodPost, ProxyChecksURL, ¶ms) - var proxyCheck ProxyCheck - _, err := s.client.Do(req, &proxyCheck) - if err != nil { - return nil, err - } - return &proxyCheck, nil -} diff --git a/clerk/proxy_checks_test.go b/clerk/proxy_checks_test.go deleted file mode 100644 index db8473c4..00000000 --- a/clerk/proxy_checks_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package clerk - -import ( - "encoding/json" - "fmt" - "net/http" - "reflect" - "testing" -) - -func TestProxyChecksService_Create(t *testing.T) { - client, mux, _, teardown := setup("token") - defer teardown() - var params CreateProxyCheckParams - payload := `{ - "proxy_url":"https://example.com/__clerk", - "domain_id": "dmn_1mebQggrD3xO5JfuHk7clQ94ysA" -}` - _ = json.Unmarshal([]byte(payload), ¶ms) - - mux.HandleFunc("/proxy_checks", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "POST") - testHeader(t, req, "Authorization", "Bearer token") - fmt.Fprint(w, dummyProxyCheckJSON) - }) - - got, err := client.ProxyChecks().Create(params) - if err != nil { - t.Fatal(err) - } - - var want ProxyCheck - err = json.Unmarshal([]byte(dummyProxyCheckJSON), &want) - if err != nil { - t.Fatal(err) - } - - if !reflect.DeepEqual(got, &want) { - t.Errorf("Response = %v, want %v", got, &want) - } -} - -const dummyProxyCheckJSON = `{ - "object": "proxy_check", - "id": "proxychk_1mebQggrD3xO5JfuHk7clQ94ysA", - "successful": true, - "domain_id": "dmn_1mebQggrD3xO5JfuHk7clQ94ysA", - "proxy_url": "https://example.com/__clerk", - "last_run_at": 1610783813, - "created_at": 1610783813, - "updated_at": 1610783813 -}` diff --git a/clerk/redirect_urls.go b/clerk/redirect_urls.go deleted file mode 100644 index 4c294b19..00000000 --- a/clerk/redirect_urls.go +++ /dev/null @@ -1,50 +0,0 @@ -package clerk - -import "net/http" - -type RedirectURLsService service - -type RedirectURLResponse struct { - Object string `json:"object"` - ID string `json:"id"` - URL string `json:"url"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` -} - -type CreateRedirectURLParams struct { - URL string `json:"url"` -} - -func (s *RedirectURLsService) Create(params CreateRedirectURLParams) (*RedirectURLResponse, error) { - req, _ := s.client.NewRequest(http.MethodPost, RedirectURLsUrl, ¶ms) - - var redirectURLResponse RedirectURLResponse - _, err := s.client.Do(req, &redirectURLResponse) - if err != nil { - return nil, err - } - return &redirectURLResponse, nil -} - -func (s *RedirectURLsService) ListAll() ([]*RedirectURLResponse, error) { - req, _ := s.client.NewRequest(http.MethodGet, RedirectURLsUrl, nil) - - var redirectURLResponses []*RedirectURLResponse - _, err := s.client.Do(req, &redirectURLResponses) - if err != nil { - return nil, err - } - return redirectURLResponses, nil -} - -func (s *RedirectURLsService) Delete(redirectURLID string) (*DeleteResponse, error) { - req, _ := s.client.NewRequest(http.MethodDelete, RedirectURLsUrl+"/"+redirectURLID, nil) - - var deleteResponse DeleteResponse - _, err := s.client.Do(req, &deleteResponse) - if err != nil { - return nil, err - } - return &deleteResponse, nil -} diff --git a/clerk/redirect_urls_test.go b/clerk/redirect_urls_test.go deleted file mode 100644 index efe8d9f2..00000000 --- a/clerk/redirect_urls_test.go +++ /dev/null @@ -1,106 +0,0 @@ -package clerk - -import ( - "encoding/json" - "fmt" - "net/http" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestRedirectURLService_Create_happyPath(t *testing.T) { - token := "token" - var redirectURLResponse RedirectURLResponse - _ = json.Unmarshal([]byte(dummyRedirectURLJson), &redirectURLResponse) - - client, mux, _, teardown := setup(token) - defer teardown() - - mux.HandleFunc("/redirect_urls", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, http.MethodPost) - testHeader(t, req, "Authorization", "Bearer "+token) - fmt.Fprint(w, dummyRedirectURLJson) - }) - - got, err := client.RedirectURLs().Create(CreateRedirectURLParams{ - URL: redirectURLResponse.URL, - }) - - assert.Nil(t, err) - assert.Equal(t, *got, redirectURLResponse) -} - -func TestRedirectURLService_Create_invalidServer(t *testing.T) { - client, _ := NewClient("token") - - _, err := client.RedirectURLs().Create(CreateRedirectURLParams{ - URL: "example.com", - }) - if err == nil { - t.Errorf("Expected error to be returned") - } -} - -func TestRedirectURLService_ListAll_happyPath(t *testing.T) { - token := "token" - var redirectURLResponse RedirectURLResponse - _ = json.Unmarshal([]byte(dummyRedirectURLJson), &redirectURLResponse) - - client, mux, _, teardown := setup(token) - defer teardown() - - mux.HandleFunc("/redirect_urls", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, http.MethodGet) - testHeader(t, req, "Authorization", "Bearer "+token) - fmt.Fprint(w, "["+dummyRedirectURLJson+"]") - }) - - got, err := client.RedirectURLs().ListAll() - - assert.Nil(t, err) - assert.Equal(t, got, []*RedirectURLResponse{&redirectURLResponse}) -} - -func TestRedirectURLService_ListAll_invalidServer(t *testing.T) { - client, _ := NewClient("token") - - _, err := client.RedirectURLs().ListAll() - if err == nil { - t.Errorf("Expected error to be returned") - } -} - -func TestRedirectURLService_Delete_happyPath(t *testing.T) { - token := "token" - client, mux, _, teardown := setup(token) - defer teardown() - - id := "some_id" - mux.HandleFunc("/redirect_urls/"+id, func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, http.MethodDelete) - testHeader(t, req, "Authorization", "Bearer "+token) - - response := fmt.Sprintf(`{ "deleted": true, "id": "%v", "object": "user" }`, id) - fmt.Fprint(w, response) - }) - - got, err := client.RedirectURLs().Delete(id) - assert.Nil(t, err) - assert.NotNil(t, got) -} - -func TestRedirectURLService_Delete_invalidServer(t *testing.T) { - client, _ := NewClient("token") - - _, err := client.RedirectURLs().Delete("random_id") - if err == nil { - t.Errorf("Expected error to be returned") - } -} - -const dummyRedirectURLJson = `{ - "object": "redirect_url", - "id": "ru_1mvFol71HiKCcypBd6xxg0IpMBN", - "url": "example.com" -}` diff --git a/clerk/saml_connections.go b/clerk/saml_connections.go deleted file mode 100644 index 9efe85bb..00000000 --- a/clerk/saml_connections.go +++ /dev/null @@ -1,140 +0,0 @@ -package clerk - -import ( - "fmt" - "net/http" -) - -type SAMLConnectionsService service - -type SAMLConnection struct { - ID string `json:"id"` - Object string `json:"object"` - Name string `json:"name"` - Domain string `json:"domain"` - IdpEntityID *string `json:"idp_entity_id"` - IdpSsoURL *string `json:"idp_sso_url"` - IdpCertificate *string `json:"idp_certificate"` - AcsURL string `json:"acs_url"` - SPEntityID string `json:"sp_entity_id"` - SPMetadataURL string `json:"sp_metadata_url"` - Active bool `json:"active"` - Provider string `json:"provider"` - UserCount int64 `json:"user_count"` - SyncUserAttributes bool `json:"sync_user_attributes"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` -} - -type ListSAMLConnectionsResponse struct { - Data []SAMLConnection `json:"data"` - TotalCount int64 `json:"total_count"` -} - -type ListSAMLConnectionsParams struct { - Limit *int - Offset *int - Query *string - OrderBy *string -} - -func (s SAMLConnectionsService) ListAll(params ListSAMLConnectionsParams) (*ListSAMLConnectionsResponse, error) { - req, err := s.client.NewRequest(http.MethodGet, SAMLConnectionsUrl) - if err != nil { - return nil, err - } - - query := req.URL.Query() - addPaginationParams(query, PaginationParams{Limit: params.Limit, Offset: params.Offset}) - - if params.Query != nil { - query.Set("query", *params.Query) - } - if params.OrderBy != nil { - query.Set("order_by", *params.OrderBy) - } - - req.URL.RawQuery = query.Encode() - - samlConnections := &ListSAMLConnectionsResponse{} - if _, err = s.client.Do(req, samlConnections); err != nil { - return nil, err - } - - return samlConnections, nil -} - -func (s SAMLConnectionsService) Read(id string) (*SAMLConnection, error) { - req, err := s.client.NewRequest(http.MethodGet, fmt.Sprintf("%s/%s", SAMLConnectionsUrl, id)) - if err != nil { - return nil, err - } - - samlConnection := &SAMLConnection{} - if _, err = s.client.Do(req, samlConnection); err != nil { - return nil, err - } - - return samlConnection, nil -} - -type CreateSAMLConnectionParams struct { - Name string `json:"name"` - Domain string `json:"domain"` - Provider string `json:"provider"` - IdpEntityID *string `json:"idp_entity_id,omitempty"` - IdpSsoURL *string `json:"idp_sso_url,omitempty"` - IdpCertificate *string `json:"idp_certificate,omitempty"` -} - -func (s SAMLConnectionsService) Create(params *CreateSAMLConnectionParams) (*SAMLConnection, error) { - req, err := s.client.NewRequest(http.MethodPost, SAMLConnectionsUrl, params) - if err != nil { - return nil, err - } - - resp := SAMLConnection{} - if _, err = s.client.Do(req, &resp); err != nil { - return nil, err - } - - return &resp, nil -} - -type UpdateSAMLConnectionParams struct { - Name *string `json:"name,omitempty"` - Domain *string `json:"domain,omitempty"` - IdpEntityID *string `json:"idp_entity_id,omitempty"` - IdpSsoURL *string `json:"idp_sso_url,omitempty"` - IdpCertificate *string `json:"idp_certificate,omitempty"` - Active *bool `json:"active,omitempty"` - SyncUserAttributes *bool `json:"sync_user_attributes,omitempty"` -} - -func (s SAMLConnectionsService) Update(id string, params *UpdateSAMLConnectionParams) (*SAMLConnection, error) { - req, err := s.client.NewRequest(http.MethodPatch, fmt.Sprintf("%s/%s", SAMLConnectionsUrl, id), params) - if err != nil { - return nil, err - } - - resp := SAMLConnection{} - if _, err = s.client.Do(req, &resp); err != nil { - return nil, err - } - - return &resp, nil -} - -func (s SAMLConnectionsService) Delete(id string) (*DeleteResponse, error) { - reqURL := fmt.Sprintf("%s/%s", SAMLConnectionsUrl, id) - req, err := s.client.NewRequest(http.MethodDelete, reqURL) - if err != nil { - return nil, err - } - - resp := &DeleteResponse{} - if _, err = s.client.Do(req, resp); err != nil { - return nil, err - } - return resp, nil -} diff --git a/clerk/saml_connections_test.go b/clerk/saml_connections_test.go deleted file mode 100644 index 388b9d0b..00000000 --- a/clerk/saml_connections_test.go +++ /dev/null @@ -1,214 +0,0 @@ -package clerk - -import ( - "encoding/json" - "fmt" - "net/http" - "net/url" - "reflect" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestSAMLConnectionsService_ListAll(t *testing.T) { - c, mux, _, teardown := setup("token") - defer teardown() - - dummyResponse := fmt.Sprintf(`{ - "data": [%s], - "total_count": 1 - }`, dummySAMLConnectionJSON) - - mux.HandleFunc("/saml_connections", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, http.MethodGet) - testHeader(t, req, "Authorization", "Bearer token") - - expectedQuery := url.Values{ - "limit": {"5"}, - "offset": {"6"}, - "query": {"my-query"}, - "order_by": {"created_at"}, - } - assert.Equal(t, expectedQuery, req.URL.Query()) - - _, _ = fmt.Fprint(w, dummyResponse) - }) - - listParams := ListSAMLConnectionsParams{ - Limit: intToPtr(5), - Offset: intToPtr(6), - Query: stringToPtr("my-query"), - OrderBy: stringToPtr("created_at"), - } - - got, err := c.SAMLConnections().ListAll(listParams) - assert.NoError(t, err) - - expected := &ListSAMLConnectionsResponse{} - _ = json.Unmarshal([]byte(dummyResponse), expected) - - if !reflect.DeepEqual(got, expected) { - t.Errorf("Response = %v, want %v", got, expected) - } -} - -func TestSAMLConnectionsService_Read(t *testing.T) { - dummyResponse := dummySAMLConnectionJSON - - c, mux, _, teardown := setup("token") - defer teardown() - - url := fmt.Sprintf("/saml_connections/%s", dummySAMLConnectionID) - - mux.HandleFunc(url, func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, http.MethodGet) - testHeader(t, req, "Authorization", "Bearer token") - _, _ = fmt.Fprint(w, dummyResponse) - }) - - got, err := c.SAMLConnections().Read(dummySAMLConnectionID) - assert.NoError(t, err) - - expected := SAMLConnection{} - _ = json.Unmarshal([]byte(dummyResponse), &expected) - - if !reflect.DeepEqual(*got, expected) { - t.Errorf("Response = %v, want %v", got, expected) - } -} - -func TestSAMLConnectionsService_Create(t *testing.T) { - dummyResponse := dummySAMLConnectionJSON - - c, mux, _, teardown := setup("token") - defer teardown() - - mux.HandleFunc("/saml_connections", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, http.MethodPost) - testHeader(t, req, "Authorization", "Bearer token") - _, _ = fmt.Fprint(w, dummyResponse) - }) - - createParams := &CreateSAMLConnectionParams{ - Name: "Testing SAML", - Domain: "example.com", - Provider: "saml_custom", - IdpEntityID: stringToPtr("test-idp-entity-id"), - IdpSsoURL: stringToPtr("https://example.com/saml/sso"), - IdpCertificate: stringToPtr(dummySAMLConnectionCertificate), - } - - got, err := c.SAMLConnections().Create(createParams) - assert.NoError(t, err) - - expected := SAMLConnection{} - _ = json.Unmarshal([]byte(dummyResponse), &expected) - - if !reflect.DeepEqual(*got, expected) { - t.Errorf("Response = %v, want %v", got, expected) - } -} - -func TestSAMLConnectionsService_Update(t *testing.T) { - expectedName := "New name for Testing SAML" - expectedActive := true - expectedSyncUserAttributes := false - dummyResponse := dummySAMLConnectionUpdatedJSON - - c, mux, _, teardown := setup("token") - defer teardown() - - url := fmt.Sprintf("/saml_connections/%s", dummySAMLConnectionID) - - mux.HandleFunc(url, func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, http.MethodPatch) - testHeader(t, req, "Authorization", "Bearer token") - _, _ = fmt.Fprint(w, dummyResponse) - }) - - updateParams := &UpdateSAMLConnectionParams{ - Name: &expectedName, - Active: &expectedActive, - SyncUserAttributes: &expectedSyncUserAttributes, - } - - got, err := c.SAMLConnections().Update(dummySAMLConnectionID, updateParams) - assert.NoError(t, err) - - expected := SAMLConnection{} - _ = json.Unmarshal([]byte(dummyResponse), &expected) - - if !reflect.DeepEqual(*got, expected) { - t.Errorf("Response = %v, want %v", got, expected) - } -} - -func TestSAMLConnectionsService_Delete(t *testing.T) { - c, mux, _, teardown := setup("token") - defer teardown() - - url := fmt.Sprintf("/saml_connections/%s", dummySAMLConnectionID) - - mux.HandleFunc(url, func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, http.MethodDelete) - testHeader(t, req, "Authorization", "Bearer token") - response := fmt.Sprintf(`{ "deleted": true, "id": "%s", "object": "saml_connection" }`, dummySAMLConnectionID) - _, _ = fmt.Fprint(w, response) - }) - - expected := DeleteResponse{ - ID: dummySAMLConnectionID, - Object: "saml_connection", - Deleted: true, - } - - got, err := c.SAMLConnections().Delete(dummySAMLConnectionID) - assert.NoError(t, err) - - if !reflect.DeepEqual(*got, expected) { - t.Errorf("Response = %v, want %v", *got, expected) - } -} - -const ( - dummySAMLConnectionID = "samlc_2P17P4pXsx8MmunM1pkeYeimDDd" - - dummySAMLConnectionJSON = ` -{ - "object": "saml_connection", - "id": "` + dummySAMLConnectionID + `", - "name": "Testing SAML", - "domain": "example.com", - "idp_entity_id": "test-idp-entity-id", - "idp_sso_url": "https://example.com/saml/sso", - "idp_certificate": "` + dummySAMLConnectionCertificate + `", - "acs_url": "` + "https://clerk.example.com/v1/saml/acs/" + dummySAMLConnectionID + `", - "sp_entity_id": "` + "https://clerk.example.com/saml/" + dummySAMLConnectionID + `", - "sp_metadata_url": "` + "https://clerk.example.com/v1/saml/metadata/" + dummySAMLConnectionID + `", - "active": false, - "provider": "saml_custom", - "user_count": 3, - "sync_user_attributes": true -}` - - dummySAMLConnectionUpdatedJSON = ` -{ - "object": "saml_connection", - "id": "` + dummySAMLConnectionID + `", - "name": "New name for Testing SAML", - "domain": "example.com", - "idp_entity_id": "test-idp-entity-id", - "idp_sso_url": "https://example.com/saml/sso", - "idp_certificate": "` + dummySAMLConnectionCertificate + `", - "acs_url": "` + "https://clerk.example.com/v1/saml/acs/" + dummySAMLConnectionID + `", - "sp_entity_id": "` + "https://clerk.example.com/saml/" + dummySAMLConnectionID + `", - "sp_metadata_url": "` + "https://clerk.example.com/v1/saml/metadata/" + dummySAMLConnectionID + `", - "active": true, - "provider": "saml_custom", - "user_count": 3, - "sync_user_attributes": false -}` - - dummySAMLConnectionCertificate = `MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=` -) diff --git a/clerk/session_claims.go b/clerk/session_claims.go deleted file mode 100644 index d9680c0a..00000000 --- a/clerk/session_claims.go +++ /dev/null @@ -1,38 +0,0 @@ -package clerk - -import ( - "encoding/json" - - "github.com/go-jose/go-jose/v3/jwt" -) - -type SessionClaims struct { - jwt.Claims - SessionID string `json:"sid"` - AuthorizedParty string `json:"azp"` - ActiveOrganizationID string `json:"org_id"` - ActiveOrganizationSlug string `json:"org_slug"` - ActiveOrganizationRole string `json:"org_role"` - ActiveOrganizationPermissions []string `json:"org_permissions"` - Actor json.RawMessage `json:"act,omitempty"` -} - -// HasPermission checks if the user has the specific permission -// in their session claims. -func (s *SessionClaims) HasPermission(permission string) bool { - for _, sessPermission := range s.ActiveOrganizationPermissions { - if sessPermission == permission { - return true - } - } - return false -} - -// HasRole checks if the user has the specific role -// in their session claims. -// Performing role checks is not considered a best-practice and -// developers should avoid it as much as possible. -// Usually, complex role checks can be refactored with a single permission check. -func (s *SessionClaims) HasRole(role string) bool { - return s.ActiveOrganizationRole == role -} diff --git a/clerk/session_claims_test.go b/clerk/session_claims_test.go deleted file mode 100644 index d3ebb6b1..00000000 --- a/clerk/session_claims_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package clerk - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestSessionClaims_HasPermissiont(t *testing.T) { - // user has permission - hasPermission := dummySessionClaims.HasPermission("org:billing:manage") - assert.True(t, hasPermission) - - // user has second permission - hasPermission = dummySessionClaims.HasPermission("org:report:view") - assert.True(t, hasPermission) - - // user does not have permission - hasPermission = dummySessionClaims.HasPermission("org:billing:create") - assert.False(t, hasPermission) -} - -func TestSessionClaims_HasRole(t *testing.T) { - // user has role - hasRole := dummySessionClaims.HasRole("org_role") - assert.True(t, hasRole) - - // user does not have role - hasRole = dummySessionClaims.HasRole("org_role_nonexistent") - assert.False(t, hasRole) -} diff --git a/clerk/sessions.go b/clerk/sessions.go deleted file mode 100644 index 71e83df7..00000000 --- a/clerk/sessions.go +++ /dev/null @@ -1,115 +0,0 @@ -package clerk - -import "fmt" - -type SessionsService service - -type Session struct { - Object string `json:"object"` - ID string `json:"id"` - ClientID string `json:"client_id"` - UserID string `json:"user_id"` - Status string `json:"status"` - LastActiveAt int64 `json:"last_active_at"` - LastActiveOrganizationID string `json:"last_active_organization_id,omitempty"` - ExpireAt int64 `json:"expire_at"` - AbandonAt int64 `json:"abandon_at"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` -} - -type ListAllSessionsParams struct { - Limit *int - Offset *int - ClientID *string - UserID *string - Status *SessionStatus -} - -type SessionStatus string - -const ( - SessionStatusAbandoned SessionStatus = "abandoned" - SessionStatusActive SessionStatus = "active" - SessionStatusEnded SessionStatus = "ended" - SessionStatusExpired SessionStatus = "expired" - SessionStatusRemoved SessionStatus = "removed" - SessionStatusReplaced SessionStatus = "replaced" - SessionStatusRevoked SessionStatus = "revoked" -) - -func (s *SessionsService) ListAll() ([]Session, error) { - sessionsUrl := "sessions" - req, _ := s.client.NewRequest("GET", sessionsUrl) - - var sessions []Session - _, err := s.client.Do(req, &sessions) - if err != nil { - return nil, err - } - return sessions, nil -} - -func (s *SessionsService) ListAllWithFiltering(params ListAllSessionsParams) ([]Session, error) { - sessionsUrl := "sessions" - req, _ := s.client.NewRequest("GET", sessionsUrl) - - paginationParams := PaginationParams{Limit: params.Limit, Offset: params.Offset} - query := req.URL.Query() - addPaginationParams(query, paginationParams) - - if params.ClientID != nil { - query.Add("client_id", *params.ClientID) - } - if params.UserID != nil { - query.Add("user_id", *params.UserID) - } - if params.Status != nil { - status := string(*params.Status) - query.Add("status", status) - } - - req.URL.RawQuery = query.Encode() - - var sessions []Session - _, err := s.client.Do(req, &sessions) - if err != nil { - return nil, err - } - return sessions, nil -} - -func (s *SessionsService) Read(sessionId string) (*Session, error) { - sessionUrl := fmt.Sprintf("%s/%s", SessionsUrl, sessionId) - req, _ := s.client.NewRequest("GET", sessionUrl) - - var session Session - _, err := s.client.Do(req, &session) - if err != nil { - return nil, err - } - return &session, nil -} - -func (s *SessionsService) Revoke(sessionId string) (*Session, error) { - sessionUrl := fmt.Sprintf("%s/%s/revoke", SessionsUrl, sessionId) - req, _ := s.client.NewRequest("POST", sessionUrl) - - var session Session - _, err := s.client.Do(req, &session) - if err != nil { - return nil, err - } - return &session, nil -} - -func (s *SessionsService) Verify(sessionId, token string) (*Session, error) { - verifyUrl := fmt.Sprintf("%s/%s/verify", SessionsUrl, sessionId) - var sessionResponse Session - - err := doVerify(s.client, verifyUrl, token, &sessionResponse) - if err != nil { - return nil, err - } - return &sessionResponse, nil -} diff --git a/clerk/sessions_test.go b/clerk/sessions_test.go deleted file mode 100644 index 7314463f..00000000 --- a/clerk/sessions_test.go +++ /dev/null @@ -1,254 +0,0 @@ -package clerk - -import ( - "encoding/json" - "fmt" - "net/http" - "net/url" - "reflect" - "testing" -) - -func TestSessionsService_ListAll_happyPath_noParams(t *testing.T) { - client, mux, _, teardown := setup("token") - defer teardown() - - expectedResponse := "[" + dummySessionJson + "]" - - mux.HandleFunc("/sessions", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "GET") - testHeader(t, req, "Authorization", "Bearer token") - fmt.Fprint(w, expectedResponse) - }) - - var want []Session - _ = json.Unmarshal([]byte(expectedResponse), &want) - - got, _ := client.Sessions().ListAll() - if len(got) != len(want) { - t.Errorf("Was expecting %d sessions to be returned, instead got %d", len(want), len(got)) - } - - if !reflect.DeepEqual(got, want) { - t.Errorf("Response = %v, want %v", got, want) - } -} - -func TestSessionsService_ListAll_pagination_and_filtering_params(t *testing.T) { - client, mux, _, teardown := setup("token") - defer teardown() - - expectedResponse := "[" + dummySessionJson + "]" - - mux.HandleFunc("/sessions", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "GET") - testHeader(t, req, "Authorization", "Bearer token") - - queryParams := url.Values{ - "limit": {}, - "offset": {}, - "client_id": {}, - "user_id": {}, - "status": {}, - } - - testQuery(t, req, queryParams) - fmt.Fprint(w, expectedResponse) - }) - - var want []Session - _ = json.Unmarshal([]byte(expectedResponse), &want) - - limit := 2 - offset := 2 - status := SessionStatusEnded - userId := "user_1mebQggrD3xO5JfuHk7clQ94ysA" - clientId := "client_1mebPYz8NFNA17fi7NemNXIwp1p" - - got, _ := client.Sessions().ListAllWithFiltering(ListAllSessionsParams{ - Limit: &limit, - Offset: &offset, - Status: &status, - UserID: &userId, - ClientID: &clientId, - }) - - if len(got) != len(want) { - t.Errorf("Was expecting %d sessions to be returned, instead got %d", len(want), len(got)) - } - - if !reflect.DeepEqual(got, want) { - t.Errorf("Response = %v, want %v", got, want) - } -} - -func TestSessionsService_ListAll_pagination_and_filtering_empty_params(t *testing.T) { - client, mux, _, teardown := setup("token") - defer teardown() - - expectedResponse := "[" + dummySessionJson + "]" - - mux.HandleFunc("/sessions", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "GET") - testHeader(t, req, "Authorization", "Bearer token") - - queryParams := url.Values{} - - testQuery(t, req, queryParams) - fmt.Fprint(w, expectedResponse) - }) - - var want []Session - _ = json.Unmarshal([]byte(expectedResponse), &want) - - limit := 2 - offset := 2 - status := SessionStatusEnded - userId := "user_1mebQggrD3xO5JfuHk7clQ94ysA" - clientId := "client_1mebPYz8NFNA17fi7NemNXIwp1p" - - got, _ := client.Sessions().ListAllWithFiltering(ListAllSessionsParams{ - Limit: &limit, - Offset: &offset, - Status: &status, - UserID: &userId, - ClientID: &clientId, - }) - - if len(got) != len(want) { - t.Errorf("Was expecting %d sessions to be returned, instead got %d", len(want), len(got)) - } - - if !reflect.DeepEqual(got, want) { - t.Errorf("Response = %v, want %v", got, want) - } -} - -func TestSessionsService_ListAll_invalidServer(t *testing.T) { - client, _ := NewClient("token") - - sessions, err := client.Sessions().ListAll() - if err == nil { - t.Errorf("Expected error to be returned") - } - if sessions != nil { - t.Errorf("Was not expecting any sessions to be returned, instead got %v", sessions) - } -} - -func TestSessionsService_Read_happyPath(t *testing.T) { - token := "token" - sessionId := "someSessionId" - expectedResponse := dummySessionJson - - client, mux, _, teardown := setup(token) - defer teardown() - - mux.HandleFunc("/sessions/"+sessionId, func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "GET") - testHeader(t, req, "Authorization", "Bearer "+token) - fmt.Fprint(w, expectedResponse) - }) - - var want Session - _ = json.Unmarshal([]byte(expectedResponse), &want) - - got, _ := client.Sessions().Read(sessionId) - if !reflect.DeepEqual(*got, want) { - t.Errorf("Response = %v, want %v", *got, want) - } -} - -func TestSessionsService_Read_invalidServer(t *testing.T) { - client, _ := NewClient("token") - - session, err := client.Sessions().Read("someSessionId") - if err == nil { - t.Errorf("Expected error to be returned") - } - if session != nil { - t.Errorf("Was not expecting any session to be returned, instead got %v", session) - } -} - -func TestSessionsService_Revoke_happyPath(t *testing.T) { - token := "token" - sessionId := "someSessionId" - expectedResponse := dummySessionJson - - client, mux, _, teardown := setup(token) - defer teardown() - - mux.HandleFunc("/sessions/"+sessionId+"/revoke", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "POST") - testHeader(t, req, "Authorization", "Bearer "+token) - fmt.Fprint(w, expectedResponse) - }) - - var want Session - _ = json.Unmarshal([]byte(expectedResponse), &want) - - got, _ := client.Sessions().Revoke(sessionId) - if !reflect.DeepEqual(*got, want) { - t.Errorf("Response = %v, want %v", *got, want) - } -} - -func TestSessionsService_Revoke_invalidServer(t *testing.T) { - client, _ := NewClient("token") - - session, err := client.Sessions().Revoke("someSessionId") - if err == nil { - t.Errorf("Expected error to be returned") - } - if session != nil { - t.Errorf("Was not expecting any session to be returned, instead got %v", session) - } -} - -func TestSessionsService_Verify_happyPath(t *testing.T) { - token := "token" - sessionId := "someSessionId" - sessionToken := "someSessionToken" - expectedResponse := dummySessionJson - - client, mux, _, teardown := setup(token) - defer teardown() - - mux.HandleFunc("/sessions/"+sessionId+"/verify", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "POST") - testHeader(t, req, "Authorization", "Bearer "+token) - fmt.Fprint(w, expectedResponse) - }) - - var want Session - _ = json.Unmarshal([]byte(expectedResponse), &want) - - got, _ := client.Sessions().Verify(sessionId, sessionToken) - if !reflect.DeepEqual(*got, want) { - t.Errorf("Response = %v, want %v", *got, want) - } -} - -func TestSessionsService_Verify_invalidServer(t *testing.T) { - client, _ := NewClient("token") - - session, err := client.Sessions().Verify("someSessionId", "someSessionToken") - if err == nil { - t.Errorf("Expected error to be returned") - } - if session != nil { - t.Errorf("Was not expecting any session to be returned, instead got %v", session) - } -} - -const dummySessionJson = `{ - "abandon_at": 1612448988, - "client_id": "client_1mebPYz8NFNA17fi7NemNXIwp1p", - "expire_at": 1610461788, - "id": "sess_1mebQdHlQI14cjxln4e2eXNzwzi", - "last_active_at": 1609857251, - "object": "session", - "status": "ended", - "user_id": "user_1mebQggrD3xO5JfuHk7clQ94ysA" - }` diff --git a/clerk/templates.go b/clerk/templates.go deleted file mode 100644 index d990d4ce..00000000 --- a/clerk/templates.go +++ /dev/null @@ -1,154 +0,0 @@ -package clerk - -import "fmt" - -type TemplatesService service - -type Template struct { - Object string `json:"object"` - Slug string `json:"slug"` - ResourceType string `json:"resource_type"` - TemplateType string `json:"template_type"` - Name string `json:"name"` - Position int `json:"position"` - CanRevert bool `json:"can_revert"` - CanDelete bool `json:"can_delete"` - FromEmailName *string `json:"from_email_name"` - DeliveredByClerk bool `json:"delivered_by_clerk"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` -} - -type TemplateExtended struct { - *Template - Subject string `json:"subject"` - Markup string `json:"markup"` - Body string `json:"body"` - AvailableVariables []string `json:"available_variables"` - RequiredVariables []string `json:"required_variables"` -} - -type TemplatePreview struct { - Subject string `json:"subject,omitempty"` - Body string `json:"body"` - FromEmailAddress *string `json:"from_email_address,omitempty"` -} - -func (s *TemplatesService) ListAll(templateType string) ([]Template, error) { - templateURL := fmt.Sprintf("%s/%s", TemplatesUrl, templateType) - req, _ := s.client.NewRequest("GET", templateURL) - - var templates []Template - - _, err := s.client.Do(req, &templates) - if err != nil { - return nil, err - } - - return templates, nil -} - -func (s *TemplatesService) Read(templateType, slug string) (*TemplateExtended, error) { - templateURL := fmt.Sprintf("%s/%s/%s", TemplatesUrl, templateType, slug) - - req, _ := s.client.NewRequest("GET", templateURL) - - var templateExtended TemplateExtended - - _, err := s.client.Do(req, &templateExtended) - if err != nil { - return nil, err - } - - return &templateExtended, nil -} - -type UpsertTemplateRequest struct { - Name string `json:"name"` - Subject string `json:"subject,omitempty"` - Markup string `json:"markup,omitempty"` - Body string `json:"body"` - FromEmailName *string `json:"from_email_name"` - DeliveredByClerk *bool `json:"delivered_by_clerk"` -} - -type PreviewTemplateRequest struct { - Subject string `json:"subject,omitempty"` - Body string `json:"body"` - FromEmailName *string `json:"from_email_name"` -} - -type ToggleDeliveryTemplateRequest struct { - DeliveredByClerk bool `json:"delivered_by_clerk"` -} - -func (s *TemplatesService) Upsert(templateType, slug string, upsertTemplateRequest *UpsertTemplateRequest) (*TemplateExtended, error) { - templateURL := fmt.Sprintf("%s/%s/%s", TemplatesUrl, templateType, slug) - req, _ := s.client.NewRequest("PUT", templateURL, upsertTemplateRequest) - - var upsertedTemplate TemplateExtended - - _, err := s.client.Do(req, &upsertedTemplate) - if err != nil { - return nil, err - } - - return &upsertedTemplate, nil -} - -// Revert reverts a user template to the corresponding system template -func (s *TemplatesService) Revert(templateType, slug string) (*TemplateExtended, error) { - templateURL := fmt.Sprintf("%s/%s/%s/revert", TemplatesUrl, templateType, slug) - req, _ := s.client.NewRequest("POST", templateURL) - - var templateExtended TemplateExtended - - _, err := s.client.Do(req, &templateExtended) - if err != nil { - return nil, err - } - - return &templateExtended, nil -} - -// Delete deletes a custom user template -func (s *TemplatesService) Delete(templateType, slug string) (*DeleteResponse, error) { - templateURL := fmt.Sprintf("%s/%s/%s", TemplatesUrl, templateType, slug) - req, _ := s.client.NewRequest("DELETE", templateURL) - - var delResponse DeleteResponse - if _, err := s.client.Do(req, &delResponse); err != nil { - return nil, err - } - - return &delResponse, nil -} - -// Preview returns a rendering of a template with sample data for preview purposes -func (s *TemplatesService) Preview(templateType, slug string, previewTemplateRequest *PreviewTemplateRequest) (*TemplatePreview, error) { - templateURL := fmt.Sprintf("%s/%s/%s/preview", TemplatesUrl, templateType, slug) - req, _ := s.client.NewRequest("POST", templateURL, previewTemplateRequest) - - var templatePreview TemplatePreview - - _, err := s.client.Do(req, &templatePreview) - if err != nil { - return nil, err - } - - return &templatePreview, nil -} - -func (s *TemplatesService) ToggleDelivery(templateType, slug string, toggleDeliveryTemplateRequest *ToggleDeliveryTemplateRequest) (*TemplateExtended, error) { - templateURL := fmt.Sprintf("%s/%s/%s/toggle_delivery", TemplatesUrl, templateType, slug) - req, _ := s.client.NewRequest("POST", templateURL, toggleDeliveryTemplateRequest) - - var toggledTemplate TemplateExtended - - _, err := s.client.Do(req, &toggledTemplate) - if err != nil { - return nil, err - } - - return &toggledTemplate, nil -} diff --git a/clerk/templates_test.go b/clerk/templates_test.go deleted file mode 100644 index 758540d5..00000000 --- a/clerk/templates_test.go +++ /dev/null @@ -1,227 +0,0 @@ -package clerk - -import ( - "encoding/json" - "fmt" - "net/http" - "reflect" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestTemplatesService_List_All_happyPath(t *testing.T) { - client, mux, _, teardown := setup("token") - defer teardown() - - templateType := "email" - - expectedResponse := "[" + dummyTemplateJSON + "]" - - url := fmt.Sprintf("/templates/%s", templateType) - - mux.HandleFunc(url, func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "GET") - testHeader(t, req, "Authorization", "Bearer token") - fmt.Fprint(w, expectedResponse) - }) - - var want []Template - _ = json.Unmarshal([]byte(expectedResponse), &want) - - got, err := client.Templates().ListAll(templateType) - assert.Nil(t, err) - - if len(got) != len(want) { - t.Errorf("Was expecting %d user to be returned, instead got %d", len(want), len(got)) - } - - if !reflect.DeepEqual(got, want) { - t.Errorf("Response = %v, want %v", got, want) - } -} - -func TestTemplatesService_Read_happyPath(t *testing.T) { - token := "token" - templateType := "email" - slug := "metalslug" - expectedResponse := dummyTemplateJSON - - client, mux, _, teardown := setup(token) - defer teardown() - - url := fmt.Sprintf("/templates/%s/%s", templateType, slug) - - mux.HandleFunc(url, func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "GET") - testHeader(t, req, "Authorization", "Bearer "+token) - fmt.Fprint(w, expectedResponse) - }) - - var want TemplateExtended - _ = json.Unmarshal([]byte(expectedResponse), &want) - - got, err := client.Templates().Read(templateType, slug) - assert.Nil(t, err) - - if !reflect.DeepEqual(*got, want) { - t.Errorf("Response = %v, want %v", *got, want) - } -} - -func TestTemplatesService_Upsert_happyPath(t *testing.T) { - token := "token" - templateType := "email" - slug := "metalslug" - expectedResponse := dummyTemplateJSON - - var payload UpsertTemplateRequest - _ = json.Unmarshal([]byte(dummyUpsertRequestJSON), &payload) - - client, mux, _, teardown := setup(token) - defer teardown() - - url := fmt.Sprintf("/templates/%s/%s", templateType, slug) - - mux.HandleFunc(url, func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "PUT") - testHeader(t, req, "Authorization", "Bearer "+token) - fmt.Fprint(w, expectedResponse) - }) - - got, err := client.Templates().Upsert(templateType, slug, &payload) - assert.Nil(t, err) - - var want TemplateExtended - _ = json.Unmarshal([]byte(expectedResponse), &want) - - if !reflect.DeepEqual(*got, want) { - t.Errorf("Response = %v, want %v", *got, payload) - } -} - -func TestTemplatesService_RevertToSystemTemplate_happyPath(t *testing.T) { - token := "token" - templateType := "email" - slug := "metalslug" - expectedResponse := dummyTemplateJSON - - client, mux, _, teardown := setup(token) - defer teardown() - - url := fmt.Sprintf("/templates/%s/%s/revert", templateType, slug) - - mux.HandleFunc(url, func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "POST") - testHeader(t, req, "Authorization", "Bearer "+token) - fmt.Fprint(w, expectedResponse) - }) - - var want TemplateExtended - _ = json.Unmarshal([]byte(expectedResponse), &want) - - got, err := client.Templates().Revert(templateType, slug) - assert.Nil(t, err) - - if !reflect.DeepEqual(*got, want) { - t.Errorf("Response = %v, want %v", *got, want) - } -} - -func TestTemplatesService_Delete_happyPath(t *testing.T) { - token := "token" - templateType := "email" - slug := "metalslug" - - client, mux, _, teardown := setup(token) - defer teardown() - - url := fmt.Sprintf("/templates/%s/%s", templateType, slug) - - mux.HandleFunc(url, func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "DELETE") - testHeader(t, req, "Authorization", "Bearer "+token) - response := fmt.Sprintf(`{ "deleted": true, "slug": "%v", "object": "template" }`, slug) - fmt.Fprint(w, response) - }) - - want := DeleteResponse{Slug: slug, Object: "template", Deleted: true} - - got, err := client.Templates().Delete(templateType, slug) - assert.Nil(t, err) - - if !reflect.DeepEqual(*got, want) { - t.Errorf("Response = %v, want %v", *got, want) - } -} - -func TestTemplatesService_Preview_happyPath(t *testing.T) { - token := "token" - templateType := "sms" - slug := "snail" - expectedResponse := dummyPreviewResponseJSON - - var payload PreviewTemplateRequest - _ = json.Unmarshal([]byte(dummyPreviewRequestJSON), &payload) - - client, mux, _, teardown := setup(token) - defer teardown() - - url := fmt.Sprintf("/templates/%s/%s/preview", templateType, slug) - - mux.HandleFunc(url, func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "POST") - testHeader(t, req, "Authorization", "Bearer "+token) - fmt.Fprint(w, expectedResponse) - }) - - got, err := client.Templates().Preview(templateType, slug, &payload) - assert.Nil(t, err) - - var want TemplatePreview - _ = json.Unmarshal([]byte(expectedResponse), &want) - - if !reflect.DeepEqual(*got, want) { - t.Errorf("Response = %v, want %v", *got, payload) - } -} - -const dummyTemplateJSON = `{ - "object": "template", - "slug": "derp", - "resource_type": "user", - "template_type": "email", - "name": "Vin Diesel", - "position": 0, - "created_at": 1633541368454, - "updated_at": 1633541368454, - "subject": "Choo choo train", - "markup": "

Hee Hee

", - "body": "

Ho Ho

", - "from_email_name": "noreply", - "delivered_by_clerk": true, - "available_variables": [ - "michael", - "jackson" - ], - "required_variables": [ - "michael" - ] -}` - -const dummyUpsertRequestJSON = `{ - "name": "Dominic Toretto", - "subject": "NOS bottles for sale", - "markup": "

Family

", - "body": "

One quarter of a mile at a time

", - "from_email_name": "sales", - "delivered_by_clerk": false -}` - -const dummyPreviewRequestJSON = `{ - "body": "{{OTPCode}} is your code for {{AppName}}, valid for {{TTLMinutes}} minutes" -}` - -const dummyPreviewResponseJSON = `{ - "body": "123456 is your code for ACME, valid for 10 minutes" -}` diff --git a/clerk/tokens.go b/clerk/tokens.go deleted file mode 100644 index fc5fa2c0..00000000 --- a/clerk/tokens.go +++ /dev/null @@ -1,129 +0,0 @@ -package clerk - -import ( - "fmt" - "time" - - "github.com/go-jose/go-jose/v3" - "github.com/go-jose/go-jose/v3/jwt" -) - -var standardClaimsKeys = []string{"iss", "sub", "aud", "exp", "nbf", "iat", "jti"} - -type TokenClaims struct { - jwt.Claims - Extra map[string]interface{} -} - -// DecodeToken decodes a jwt token without verifying it. -func (c *client) DecodeToken(token string) (*TokenClaims, error) { - parsedToken, err := jwt.ParseSigned(token) - if err != nil { - return nil, err - } - - standardClaims := jwt.Claims{} - extraClaims := make(map[string]interface{}) - - if err = parsedToken.UnsafeClaimsWithoutVerification(&standardClaims, &extraClaims); err != nil { - return nil, err - } - - // Delete any standard claims included in the extra claims - for _, key := range standardClaimsKeys { - delete(extraClaims, key) - } - - return &TokenClaims{Claims: standardClaims, Extra: extraClaims}, nil -} - -type verifyTokenOptions struct { - authorizedParties map[string]struct{} - leeway time.Duration - jwk *jose.JSONWebKey - customClaims interface{} - isSatellite bool - proxyURL string -} - -// VerifyToken verifies the session jwt token. -func (c *client) VerifyToken(token string, opts ...VerifyTokenOption) (*SessionClaims, error) { - options := &verifyTokenOptions{} - - for _, opt := range opts { - if err := opt(options); err != nil { - return nil, err - } - } - - parsedToken, err := jwt.ParseSigned(token) - if err != nil { - return nil, err - } - - if len(parsedToken.Headers) == 0 { - return nil, fmt.Errorf("missing jwt headers") - } - - kid := parsedToken.Headers[0].KeyID - if kid == "" { - return nil, fmt.Errorf("missing jwt kid header claim") - } - - jwk := options.jwk - if jwk == nil { - jwk, err = c.getJWK(kid) - if err != nil { - return nil, err - } - } - - if parsedToken.Headers[0].Algorithm != jwk.Algorithm { - return nil, fmt.Errorf("invalid signing algorithm %s", jwk.Algorithm) - } - - claims := SessionClaims{} - if err = verifyTokenParseClaims(parsedToken, jwk.Key, &claims, options); err != nil { - return nil, err - } - - if err = claims.Claims.ValidateWithLeeway(jwt.Expected{Time: time.Now()}, options.leeway); err != nil { - return nil, err - } - - issuer := newIssuer(claims.Issuer). - WithSatelliteDomain(options.isSatellite). - WithProxyURL(options.proxyURL) - - if !issuer.IsValid() { - return nil, fmt.Errorf("invalid issuer %s", claims.Issuer) - } - - if claims.AuthorizedParty != "" && len(options.authorizedParties) > 0 { - if _, ok := options.authorizedParties[claims.AuthorizedParty]; !ok { - return nil, fmt.Errorf("invalid authorized party %s", claims.AuthorizedParty) - } - } - - return &claims, nil -} - -func (c *client) getJWK(kid string) (*jose.JSONWebKey, error) { - if c.jwksCache.isInvalid() { - jwks, err := c.jwks.ListAll() - if err != nil { - return nil, err - } - - c.jwksCache.set(jwks) - } - - return c.jwksCache.get(kid) -} - -func verifyTokenParseClaims(parsedToken *jwt.JSONWebToken, key interface{}, sessionClaims *SessionClaims, options *verifyTokenOptions) error { - if options.customClaims == nil { - return parsedToken.Claims(key, sessionClaims) - } - return parsedToken.Claims(key, sessionClaims, options.customClaims) -} diff --git a/clerk/tokens_issuer.go b/clerk/tokens_issuer.go deleted file mode 100644 index a6a96c3e..00000000 --- a/clerk/tokens_issuer.go +++ /dev/null @@ -1,38 +0,0 @@ -package clerk - -import "strings" - -type issuer struct { - iss string - isSatellite bool - proxyURL string -} - -func newIssuer(iss string) *issuer { - return &issuer{ - iss: iss, - } -} - -func (iss *issuer) WithSatelliteDomain(isSatellite bool) *issuer { - iss.isSatellite = isSatellite - return iss -} - -func (iss *issuer) WithProxyURL(proxyURL string) *issuer { - iss.proxyURL = proxyURL - return iss -} - -func (iss *issuer) IsValid() bool { - if iss.isSatellite { - return true - } - - if iss.proxyURL != "" { - return iss.iss == iss.proxyURL - } - - return strings.HasPrefix(iss.iss, "https://clerk.") || - strings.Contains(iss.iss, ".clerk.accounts") -} diff --git a/clerk/tokens_options.go b/clerk/tokens_options.go deleted file mode 100644 index c2692f6e..00000000 --- a/clerk/tokens_options.go +++ /dev/null @@ -1,96 +0,0 @@ -package clerk - -import ( - "crypto/x509" - "encoding/pem" - "fmt" - "strings" - "time" - - "github.com/go-jose/go-jose/v3" -) - -// VerifyTokenOption describes a functional parameter for the VerifyToken method -type VerifyTokenOption func(*verifyTokenOptions) error - -// WithAuthorizedParty allows to set the authorized parties to check against the azp claim of the session token -func WithAuthorizedParty(parties ...string) VerifyTokenOption { - return func(o *verifyTokenOptions) error { - authorizedParties := make(map[string]struct{}) - for _, party := range parties { - authorizedParties[party] = struct{}{} - } - - o.authorizedParties = authorizedParties - return nil - } -} - -// WithLeeway allows to set a custom leeway that gives some extra time to the token to accomodate for clock skew, etc. -func WithLeeway(leeway time.Duration) VerifyTokenOption { - return func(o *verifyTokenOptions) error { - o.leeway = leeway - return nil - } -} - -// WithJWTVerificationKey allows to set the JWK to use for verifying tokens without the need to download or cache any JWKs at runtime -func WithJWTVerificationKey(key string) VerifyTokenOption { - return func(o *verifyTokenOptions) error { - // From the Clerk docs: "Note that the JWT Verification key is not in - // PEM format, the header and footer are missing, in order to be shorter - // and single-line for easier setup." - if !strings.HasPrefix(key, "-----BEGIN") { - key = "-----BEGIN PUBLIC KEY-----\n" + key + "\n-----END PUBLIC KEY-----" - } - - jwk, err := pemToJWK(key) - if err != nil { - return err - } - - o.jwk = jwk - return nil - } -} - -// WithCustomClaims allows to pass a type (e.g. struct), which will be populated with the token claims based on json tags. -// For this option to work you must pass a pointer. -func WithCustomClaims(customClaims interface{}) VerifyTokenOption { - return func(o *verifyTokenOptions) error { - o.customClaims = customClaims - return nil - } -} - -func WithSatelliteDomain(isSatellite bool) VerifyTokenOption { - return func(o *verifyTokenOptions) error { - o.isSatellite = isSatellite - return nil - } -} - -func WithProxyURL(proxyURL string) VerifyTokenOption { - return func(o *verifyTokenOptions) error { - o.proxyURL = proxyURL - return nil - } -} - -func pemToJWK(key string) (*jose.JSONWebKey, error) { - block, _ := pem.Decode([]byte(key)) - if block == nil { - return nil, fmt.Errorf("invalid PEM-encoded block") - } - - if block.Type != "PUBLIC KEY" { - return nil, fmt.Errorf("invalid key type, expected a public key") - } - - rsaPublicKey, err := x509.ParsePKIXPublicKey(block.Bytes) - if err != nil { - return nil, fmt.Errorf("failed to parse public key: %v", err) - } - - return &jose.JSONWebKey{Key: rsaPublicKey, Algorithm: "RS256"}, nil -} diff --git a/clerk/tokens_options_test.go b/clerk/tokens_options_test.go deleted file mode 100644 index 66d91472..00000000 --- a/clerk/tokens_options_test.go +++ /dev/null @@ -1,110 +0,0 @@ -package clerk - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func TestWithAuthorizedPartyNone(t *testing.T) { - opts := &verifyTokenOptions{} - err := WithAuthorizedParty()(opts) - - if assert.NoError(t, err) { - assert.Len(t, opts.authorizedParties, 0) - } -} - -func TestWithAuthorizedPartySingle(t *testing.T) { - opts := &verifyTokenOptions{} - err := WithAuthorizedParty("test-party")(opts) - - assert.NoError(t, err) - assert.Len(t, opts.authorizedParties, 1) - assert.Equal(t, arrayToMap(t, []string{"test-party"}), opts.authorizedParties) -} - -func TestWithAuthorizedPartyMultiple(t *testing.T) { - authorizedParties := []string{"test-party", "another_party", "yet-another-party"} - - opts := &verifyTokenOptions{} - err := WithAuthorizedParty(authorizedParties...)(opts) - - assert.NoError(t, err) - assert.Len(t, opts.authorizedParties, len(authorizedParties)) - assert.Equal(t, arrayToMap(t, authorizedParties), opts.authorizedParties) -} - -func TestWithLeeway(t *testing.T) { - leeway := 5 * time.Second - - opts := &verifyTokenOptions{} - err := WithLeeway(leeway)(opts) - - assert.NoError(t, err) - assert.Equal(t, opts.leeway, leeway) -} - -func TestWithJWTVerificationKey(t *testing.T) { - key := "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm7Zs5PFGrsrmvys1hHkSDOYoghz9+z9o+E6WgMqR+R/Af0/QRqQo/YwCmzB+01+5Us1NdSa32YuQYiMxV4T+g3eebSiBqPNiCyjl2wttCm5LAV5iHyVqwnBNcrXlA5mRFQz8lmyfpoksNDEVzJPwwHzPjKSIKsGgsrPnw6XsyOPJY/8UocscEcHptTmahHrbfNZLN0FrMneHw9tnn2AiUctuU9bw80KwPd+WFdZ6UZF/kPxVFsANJpz1aMpz7Lxi3Sz1ztUCdHvNJitRUO1Qewby4xi9DfIEECMq78LLmwGaTiKxutC6KwHLJEcbUblOJHpYVEXdBex9xGJ/2DHrBQIDAQAB" - - opts := &verifyTokenOptions{} - err := WithJWTVerificationKey(key)(opts) - - if assert.NoError(t, err) { - assert.Equal(t, "RS256", opts.jwk.Algorithm) - } -} - -func TestWithJWTVerificationKey_PEM(t *testing.T) { - key := `-----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm7Zs5PFGrsrmvys1hHkS -DOYoghz9+z9o+E6WgMqR+R/Af0/QRqQo/YwCmzB+01+5Us1NdSa32YuQYiMxV4T+ -g3eebSiBqPNiCyjl2wttCm5LAV5iHyVqwnBNcrXlA5mRFQz8lmyfpoksNDEVzJPw -wHzPjKSIKsGgsrPnw6XsyOPJY/8UocscEcHptTmahHrbfNZLN0FrMneHw9tnn2Ai -UctuU9bw80KwPd+WFdZ6UZF/kPxVFsANJpz1aMpz7Lxi3Sz1ztUCdHvNJitRUO1Q -ewby4xi9DfIEECMq78LLmwGaTiKxutC6KwHLJEcbUblOJHpYVEXdBex9xGJ/2DHr -BQIDAQAB ------END PUBLIC KEY-----` - - opts := &verifyTokenOptions{} - err := WithJWTVerificationKey(key)(opts) - - if assert.NoError(t, err) { - assert.Equal(t, "RS256", opts.jwk.Algorithm) - } -} - -func TestWithSatelliteDomain(t *testing.T) { - isSatellite := true - - opts := &verifyTokenOptions{} - err := WithSatelliteDomain(isSatellite)(opts) - - if assert.NoError(t, err) { - assert.Equal(t, isSatellite, opts.isSatellite) - } -} - -func TestWithProxyURL(t *testing.T) { - proxyURL := "url" - - opts := &verifyTokenOptions{} - err := WithProxyURL(proxyURL)(opts) - - if assert.NoError(t, err) { - assert.Equal(t, proxyURL, opts.proxyURL) - } -} - -func arrayToMap(t *testing.T, input []string) map[string]struct{} { - t.Helper() - - output := make(map[string]struct{}) - for _, s := range input { - output[s] = struct{}{} - } - - return output -} diff --git a/clerk/tokens_test.go b/clerk/tokens_test.go deleted file mode 100644 index c4928dba..00000000 --- a/clerk/tokens_test.go +++ /dev/null @@ -1,447 +0,0 @@ -package clerk - -import ( - "crypto" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "encoding/json" - "encoding/pem" - "net/http" - "reflect" - "testing" - "time" - - "github.com/go-jose/go-jose/v3" - "github.com/go-jose/go-jose/v3/jwt" - "github.com/stretchr/testify/assert" -) - -var ( - dummyTokenClaims = map[string]interface{}{ - "iss": "issuer", - "sub": "subject", - "aud": []string{"clerk"}, - "name": "name", - "picture": "picture", - } - - dummyTokenClaimsExpected = TokenClaims{ - Claims: jwt.Claims{ - Issuer: "issuer", - Subject: "subject", - Audience: jwt.Audience{"clerk"}, - Expiry: nil, - IssuedAt: nil, - }, - Extra: map[string]interface{}{ - "name": "name", - "picture": "picture", - }, - } - - dummySessionClaims = SessionClaims{ - Claims: jwt.Claims{ - Issuer: "https://clerk.issuer", - Subject: "subject", - Audience: nil, - Expiry: nil, - IssuedAt: nil, - }, - SessionID: "session_id", - AuthorizedParty: "authorized_party", - ActiveOrganizationID: "org_id", - ActiveOrganizationSlug: "org_slug", - ActiveOrganizationRole: "org_role", - ActiveOrganizationPermissions: []string{"org:billing:manage", "org:report:view"}, - } -) - -func TestClient_DecodeToken_EmptyToken(t *testing.T) { - c, _ := NewClient("token") - - _, err := c.DecodeToken("") - if err == nil { - t.Errorf("Expected error to be returned") - } -} - -func TestClient_DecodeToken_Success(t *testing.T) { - c, _ := NewClient("token") - token, _ := testGenerateTokenJWT(t, dummyTokenClaims, "kid") - - got, _ := c.DecodeToken(token) - - if !reflect.DeepEqual(got, &dummyTokenClaimsExpected) { - t.Errorf("Expected %+v, but got %+v", &dummyTokenClaimsExpected, got) - } -} - -func TestClient_VerifyToken_EmptyToken(t *testing.T) { - c, _ := NewClient("token") - - _, err := c.VerifyToken("") - if err == nil { - t.Errorf("Expected error to be returned") - } -} - -func TestClient_VerifyToken_MissingKID(t *testing.T) { - c, _ := NewClient("token") - token, _ := testGenerateTokenJWT(t, dummySessionClaims, "") - - _, err := c.VerifyToken(token) - if err == nil { - t.Errorf("Expected error to be returned") - } -} - -func TestClient_VerifyToken_MismatchKID(t *testing.T) { - c, _ := NewClient("token") - token, pubKey := testGenerateTokenJWT(t, dummySessionClaims, "kid") - - client := c.(*client) - client.jwksCache.set(testBuildJWKS(t, pubKey, jose.RS256, "invalid-kid")) - - _, err := c.VerifyToken(token) - if err == nil { - t.Errorf("Expected error to be returned") - } -} - -func TestClient_VerifyToken_MismatchAlgorithm(t *testing.T) { - c, _ := NewClient("token") - token, pubKey := testGenerateTokenJWT(t, dummySessionClaims, "kid") - - client := c.(*client) - client.jwksCache.set(testBuildJWKS(t, pubKey, jose.RS512, "kid")) - - _, err := c.VerifyToken(token) - if err == nil { - t.Errorf("Expected error to be returned") - } -} - -func TestClient_VerifyToken_InvalidKey(t *testing.T) { - c, _ := NewClient("token") - token, _ := testGenerateTokenJWT(t, dummySessionClaims, "kid") - privKey, _ := rsa.GenerateKey(rand.Reader, 2048) - - client := c.(*client) - client.jwksCache.set(testBuildJWKS(t, privKey.Public(), jose.RS256, "kid")) - - _, err := c.VerifyToken(token) - if err == nil { - t.Errorf("Expected error to be returned") - } -} - -func TestClient_VerifyToken_InvalidIssuer(t *testing.T) { - c, _ := NewClient("token") - - claims := dummySessionClaims - claims.Issuer = "issuer" - - token, pubKey := testGenerateTokenJWT(t, claims, "kid") - - client := c.(*client) - client.jwksCache.set(testBuildJWKS(t, pubKey, jose.RS256, "kid")) - - _, err := c.VerifyToken(token) - if err == nil { - t.Errorf("Expected error to be returned") - } -} - -func TestClient_VerifyToken_IssuerSatelliteDomain(t *testing.T) { - c, _ := NewClient("token") - - token, pubKey := testGenerateTokenJWT(t, dummySessionClaims, "kid") - - client := c.(*client) - client.jwksCache.set(testBuildJWKS(t, pubKey, jose.RS256, "kid")) - - got, _ := c.VerifyToken(token, WithSatelliteDomain(true)) - if !reflect.DeepEqual(got, &dummySessionClaims) { - t.Errorf("Expected %+v, but got %+v", dummySessionClaims, got) - } -} - -func TestClient_VerifyToken_InvalidIssuerProxyURL(t *testing.T) { - c, _ := NewClient("token") - - claims := dummySessionClaims - claims.Issuer = "invalid" - - token, pubKey := testGenerateTokenJWT(t, claims, "kid") - - client := c.(*client) - client.jwksCache.set(testBuildJWKS(t, pubKey, jose.RS256, "kid")) - - _, err := c.VerifyToken(token, WithProxyURL("issuer")) - if err == nil { - t.Errorf("Expected error to be returned") - } -} - -func TestClient_VerifyToken_ValidIssuerProxyURL(t *testing.T) { - c, _ := NewClient("token") - - claims := dummySessionClaims - claims.Issuer = "issuer" - - token, pubKey := testGenerateTokenJWT(t, claims, "kid") - - client := c.(*client) - client.jwksCache.set(testBuildJWKS(t, pubKey, jose.RS256, "kid")) - - got, _ := c.VerifyToken(token, WithProxyURL("issuer")) - if !reflect.DeepEqual(got, &claims) { - t.Errorf("Expected %+v, but got %+v", claims, got) - } -} - -func TestClient_VerifyToken_ExpiredToken(t *testing.T) { - c, _ := NewClient("token") - - expiredClaims := dummySessionClaims - expiredClaims.Expiry = jwt.NewNumericDate(time.Now().Add(time.Second * -1)) - token, pubKey := testGenerateTokenJWT(t, expiredClaims, "kid") - - client := c.(*client) - client.jwksCache.set(testBuildJWKS(t, pubKey, jose.RS256, "kid")) - - _, err := c.VerifyToken(token) - if err == nil { - t.Errorf("Expected error to be returned") - } -} - -func TestClient_VerifyToken_InvalidAuthorizedParty(t *testing.T) { - c, _ := NewClient("token") - - claims := dummySessionClaims - claims.AuthorizedParty = "fake-party" - - token, pubKey := testGenerateTokenJWT(t, claims, "kid") - - client := c.(*client) - client.jwksCache.set(testBuildJWKS(t, pubKey, jose.RS256, "kid")) - - _, err := c.VerifyToken(token, WithAuthorizedParty("authorized_party")) - if err == nil { - t.Errorf("Expected error to be returned") - } -} - -func TestClient_VerifyToken_Success(t *testing.T) { - c, _ := NewClient("token") - token, pubKey := testGenerateTokenJWT(t, dummySessionClaims, "kid") - - client := c.(*client) - client.jwksCache.set(testBuildJWKS(t, pubKey, jose.RS256, "kid")) - - got, _ := c.VerifyToken(token) - if !reflect.DeepEqual(got, &dummySessionClaims) { - t.Errorf("Expected %+v, but got %+v", dummySessionClaims, got) - } -} - -func TestClient_VerifyToken_Success_NewIssuerFormat(t *testing.T) { - c, _ := NewClient("token") - - claims := dummySessionClaims - claims.Issuer = "https://foo-bar-13.clerk.accounts.dev" - - token, pubKey := testGenerateTokenJWT(t, claims, "kid") - - client := c.(*client) - client.jwksCache.set(testBuildJWKS(t, pubKey, jose.RS256, "kid")) - - got, err := c.VerifyToken(token) - if err != nil { - t.Fatalf("Expected no error but got %v", err) - } - - if !reflect.DeepEqual(got, &claims) { - t.Errorf("Expected %+v, but got %+v", claims, got) - } -} - -func TestClient_VerifyToken_Success_ExpiredCache(t *testing.T) { - c, mux, _, teardown := setup("token") - defer teardown() - - token, pubKey := testGenerateTokenJWT(t, dummySessionClaims, "kid") - - mux.HandleFunc("/jwks", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "GET") - testHeader(t, req, "Authorization", "Bearer token") - _ = json.NewEncoder(w).Encode(testBuildJWKS(t, pubKey, jose.RS256, "kid")) - }) - - client := c.(*client) - client.jwksCache.expiresAt = time.Now().Add(time.Second * -5) - - got, _ := c.VerifyToken(token) - if !reflect.DeepEqual(got, &dummySessionClaims) { - t.Errorf("Expected %+v, but got %+v", dummySessionClaims, got) - } -} - -func TestClient_VerifyToken_Success_AuthorizedParty(t *testing.T) { - c, mux, _, teardown := setup("token") - defer teardown() - - token, pubKey := testGenerateTokenJWT(t, dummySessionClaims, "kid") - - mux.HandleFunc("/jwks", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "GET") - testHeader(t, req, "Authorization", "Bearer token") - _ = json.NewEncoder(w).Encode(testBuildJWKS(t, pubKey, jose.RS256, "kid")) - }) - - client := c.(*client) - client.jwksCache.expiresAt = time.Now().Add(time.Second * -5) - - got, _ := c.VerifyToken(token, WithAuthorizedParty("authorized_party")) - if !reflect.DeepEqual(got, &dummySessionClaims) { - t.Errorf("Expected %+v, but got %+v", dummySessionClaims, got) - } -} - -func TestClient_VerifyToken_Success_WithLeeway(t *testing.T) { - c, _ := NewClient("token") - - expiredClaims := dummySessionClaims - expiredClaims.Expiry = jwt.NewNumericDate(time.Now().Add(time.Second * -1)) - token, pubKey := testGenerateTokenJWT(t, expiredClaims, "kid") - - client := c.(*client) - client.jwksCache.set(testBuildJWKS(t, pubKey, jose.RS256, "kid")) - - _, err := c.VerifyToken(token, WithLeeway(3*time.Second)) - if err != nil { - t.Errorf("Expected no error to be returned, but got: %+v", err) - } -} - -func TestClient_VerifyToken_Success_WithJWTVerificationKey(t *testing.T) { - c, _ := NewClient("token") - token, pubKey := testGenerateTokenJWT(t, dummySessionClaims, "kid") - verificationKey := testRSAPublicKeyToPEM(t, pubKey) - - _, err := c.VerifyToken(token, WithJWTVerificationKey(verificationKey)) - if err != nil { - t.Errorf("Expected no error, but got: %+v", err) - } -} - -func TestClient_VerifyToken_Success_WithCustomClaims(t *testing.T) { - c, _ := NewClient("token") - - expectedClaims := map[string]interface{}{ - "iss": "https://clerk.issuer", - "sub": "subject", - "sid": "session_id", - "azp": "authorized_party", - "role": "tester", - "interests": []string{"tennis", "football"}, - } - - token, pubKey := testGenerateTokenJWT(t, expectedClaims, "kid") - - client := c.(*client) - client.jwksCache.set(testBuildJWKS(t, pubKey, jose.RS256, "kid")) - - customClaims := struct { - Issuer string `json:"iss"` - UserID string `json:"sub"` - Role string `json:"role"` - Interests []string `json:"interests"` - }{} - - _, _ = c.VerifyToken(token, WithCustomClaims(&customClaims)) - assert.Equal(t, expectedClaims["iss"], customClaims.Issuer) - assert.Equal(t, expectedClaims["sub"], customClaims.UserID) - assert.Equal(t, expectedClaims["role"], customClaims.Role) - assert.Equal(t, expectedClaims["interests"], customClaims.Interests) -} - -func TestClient_VerifyToken_Error_WithJWTVerificationKey(t *testing.T) { - c, _ := NewClient("token") - token, _ := testGenerateTokenJWT(t, dummySessionClaims, "kid") - - // Generate new public key not matching the one from the token - _, pubKey := testGenerateTokenJWT(t, dummySessionClaims, "kid") - verificationKey := testRSAPublicKeyToPEM(t, pubKey) - - _, err := c.VerifyToken(token, WithJWTVerificationKey(verificationKey)) - if err == nil { - t.Errorf("Expected error to be returned") - } -} - -func testGenerateTokenJWT(t *testing.T, claims interface{}, kid string) (string, crypto.PublicKey) { - t.Helper() - - privKey, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - t.Error(err) - } - - signerOpts := &jose.SignerOptions{} - signerOpts.WithType("JWT") - if kid != "" { - signerOpts.WithHeader("kid", kid) - } - - signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.RS256, Key: privKey}, signerOpts) - if err != nil { - t.Error(err) - } - - builder := jwt.Signed(signer) - builder = builder.Claims(claims) - - token, err := builder.CompactSerialize() - if err != nil { - t.Error(err) - } - - return token, privKey.Public() -} - -func testBuildJWKS(t *testing.T, pubKey crypto.PublicKey, alg jose.SignatureAlgorithm, kid string) *JWKS { - t.Helper() - - return &JWKS{Keys: []jose.JSONWebKey{ - { - Key: pubKey, - KeyID: kid, - Algorithm: string(alg), - Use: "sig", - }, - }} -} - -func testRSAPublicKeyToPEM(t *testing.T, input interface{}) string { - t.Helper() - - rsaPubKey, ok := input.(*rsa.PublicKey) - if !ok { - t.Error("provided input is not an RSA public key") - } - - pubKeyBytes, err := x509.MarshalPKIXPublicKey(rsaPubKey) - if err != nil { - t.Error("failed to marshal public key") - } - - pemBytes := pem.EncodeToMemory(&pem.Block{ - Type: "PUBLIC KEY", - Bytes: pubKeyBytes, - }) - - return string(pemBytes) -} diff --git a/clerk/users.go b/clerk/users.go deleted file mode 100644 index 635da3e0..00000000 --- a/clerk/users.go +++ /dev/null @@ -1,360 +0,0 @@ -package clerk - -import ( - "encoding/json" - "fmt" - "net/http" - "strconv" -) - -type UsersService service - -type User struct { - ID string `json:"id"` - Object string `json:"object"` - Username *string `json:"username"` - FirstName *string `json:"first_name"` - LastName *string `json:"last_name"` - Gender *string `json:"gender"` - Birthday *string `json:"birthday"` - ProfileImageURL string `json:"profile_image_url"` - ImageURL *string `json:"image_url,omitempty"` - HasImage bool `json:"has_image"` - PrimaryEmailAddressID *string `json:"primary_email_address_id"` - PrimaryPhoneNumberID *string `json:"primary_phone_number_id"` - PrimaryWeb3WalletID *string `json:"primary_web3_wallet_id"` - PasswordEnabled bool `json:"password_enabled"` - TwoFactorEnabled bool `json:"two_factor_enabled"` - TOTPEnabled bool `json:"totp_enabled"` - BackupCodeEnabled bool `json:"backup_code_enabled"` - EmailAddresses []EmailAddress `json:"email_addresses"` - PhoneNumbers []PhoneNumber `json:"phone_numbers"` - Web3Wallets []Web3Wallet `json:"web3_wallets"` - ExternalAccounts []interface{} `json:"external_accounts"` - PublicMetadata interface{} `json:"public_metadata"` - PrivateMetadata interface{} `json:"private_metadata"` - UnsafeMetadata interface{} `json:"unsafe_metadata"` - LastSignInAt *int64 `json:"last_sign_in_at"` - Banned bool `json:"banned"` - Locked bool `json:"locked"` - LockoutExpiresInSeconds *int64 `json:"lockout_expires_in_seconds"` - VerificationAttemptsRemaining *int64 `json:"verification_attempts_remaining"` - ExternalID *string `json:"external_id"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` - LastActiveAt *int64 `json:"last_active_at"` -} - -type UserOAuthAccessToken struct { - Object string `json:"object"` - Token string `json:"token"` - Provider string `json:"provider"` - PublicMetadata json.RawMessage `json:"public_metadata"` - Label *string `json:"label"` - Scopes []string `json:"scopes"` - TokenSecret *string `json:"token_secret"` -} - -type IdentificationLink struct { - IdentType string `json:"type"` - IdentID string `json:"id"` -} - -type CreateUserParams struct { - EmailAddresses []string `json:"email_address,omitempty"` - PhoneNumbers []string `json:"phone_number,omitempty"` - Web3Wallets []string `json:"web3_wallet,omitempty"` - Username *string `json:"username,omitempty"` - Password *string `json:"password,omitempty"` - FirstName *string `json:"first_name,omitempty"` - LastName *string `json:"last_name,omitempty"` - ExternalID *string `json:"external_id,omitempty"` - UnsafeMetadata *json.RawMessage `json:"unsafe_metadata,omitempty"` - PublicMetadata *json.RawMessage `json:"public_metadata,omitempty"` - PrivateMetadata *json.RawMessage `json:"private_metadata,omitempty"` - PasswordDigest *string `json:"password_digest,omitempty"` - PasswordHasher *string `json:"password_hasher,omitempty"` - SkipPasswordRequirement *bool `json:"skip_password_requirement,omitempty"` - SkipPasswordChecks *bool `json:"skip_password_checks,omitempty"` - TOTPSecret *string `json:"totp_secret,omitempty"` - BackupCodes []string `json:"backup_codes,omitempty"` - // Specified in RFC3339 format - CreatedAt *string `json:"created_at,omitempty"` -} - -func (s *UsersService) Create(params CreateUserParams) (*User, error) { - req, _ := s.client.NewRequest("POST", UsersUrl, ¶ms) - var user User - _, err := s.client.Do(req, &user) - if err != nil { - return nil, err - } - return &user, nil -} - -type ListAllUsersParams struct { - Limit *int - Offset *int - EmailAddresses []string - ExternalIDs []string - PhoneNumbers []string - Web3Wallets []string - Usernames []string - UserIDs []string - Query *string - LastActiveAtSince *int64 - OrderBy *string -} - -func (s *UsersService) ListAll(params ListAllUsersParams) ([]User, error) { - req, _ := s.client.NewRequest("GET", UsersUrl) - - s.addUserSearchParamsToRequest(req, params) - - paginationParams := PaginationParams{Limit: params.Limit, Offset: params.Offset} - query := req.URL.Query() - addPaginationParams(query, paginationParams) - if params.OrderBy != nil { - query.Add("order_by", *params.OrderBy) - } - req.URL.RawQuery = query.Encode() - - var users []User - _, err := s.client.Do(req, &users) - if err != nil { - return nil, err - } - return users, nil -} - -type UserCount struct { - Object string `json:"object"` - TotalCount int `json:"total_count"` -} - -func (s *UsersService) Count(params ListAllUsersParams) (*UserCount, error) { - req, _ := s.client.NewRequest("GET", UsersCountUrl) - - s.addUserSearchParamsToRequest(req, params) - - var userCount UserCount - _, err := s.client.Do(req, &userCount) - if err != nil { - return nil, err - } - return &userCount, nil -} - -func (s *UsersService) addUserSearchParamsToRequest(r *http.Request, params ListAllUsersParams) { - query := r.URL.Query() - if params.EmailAddresses != nil { - for _, email := range params.EmailAddresses { - query.Add("email_address", email) - } - } - if params.PhoneNumbers != nil { - for _, phone := range params.PhoneNumbers { - query.Add("phone_number", phone) - } - } - if params.ExternalIDs != nil { - for _, externalID := range params.ExternalIDs { - query.Add("external_id", externalID) - } - } - if params.Web3Wallets != nil { - for _, web3Wallet := range params.Web3Wallets { - query.Add("web3_wallet", web3Wallet) - } - } - if params.Usernames != nil { - for _, username := range params.Usernames { - query.Add("username", username) - } - } - if params.UserIDs != nil { - for _, userID := range params.UserIDs { - query.Add("user_id", userID) - } - } - if params.Query != nil { - query.Add("query", *params.Query) - } - if params.LastActiveAtSince != nil { - query.Add("last_active_at_since", strconv.Itoa(int(*params.LastActiveAtSince))) - } - r.URL.RawQuery = query.Encode() -} - -func (s *UsersService) Read(userId string) (*User, error) { - userUrl := fmt.Sprintf("%s/%s", UsersUrl, userId) - req, _ := s.client.NewRequest("GET", userUrl) - - var user User - _, err := s.client.Do(req, &user) - if err != nil { - return nil, err - } - return &user, nil -} - -func (s *UsersService) Delete(userId string) (*DeleteResponse, error) { - userUrl := fmt.Sprintf("%s/%s", UsersUrl, userId) - req, _ := s.client.NewRequest("DELETE", userUrl) - - var delResponse DeleteResponse - if _, err := s.client.Do(req, &delResponse); err != nil { - return nil, err - } - return &delResponse, nil -} - -type UpdateUser struct { - FirstName *string `json:"first_name,omitempty"` - LastName *string `json:"last_name,omitempty"` - PrimaryEmailAddressID *string `json:"primary_email_address_id,omitempty"` - PrimaryPhoneNumberID *string `json:"primary_phone_number_id,omitempty"` - PrimaryWeb3WalletID *string `json:"primary_web3_wallet_id,omitempty"` - Username *string `json:"username,omitempty"` - ProfileImageID *string `json:"profile_image_id,omitempty"` - ProfileImage *string `json:"profile_image,omitempty"` - Password *string `json:"password,omitempty"` - SkipPasswordChecks *bool `json:"skip_password_checks,omitempty"` - SignOutOfOtherSessions *bool `json:"sign_out_of_other_sessions,omitempty"` - ExternalID *string `json:"external_id,omitempty"` - PublicMetadata interface{} `json:"public_metadata,omitempty"` - PrivateMetadata interface{} `json:"private_metadata,omitempty"` - UnsafeMetadata interface{} `json:"unsafe_metadata,omitempty"` - TOTPSecret *string `json:"totp_secret,omitempty"` - BackupCodes []string `json:"backup_codes,omitempty"` - // Specified in RFC3339 format - CreatedAt *string `json:"created_at,omitempty"` -} - -func (s *UsersService) Update(userId string, updateRequest *UpdateUser) (*User, error) { - userUrl := fmt.Sprintf("%s/%s", UsersUrl, userId) - req, _ := s.client.NewRequest("PATCH", userUrl, updateRequest) - - var updatedUser User - _, err := s.client.Do(req, &updatedUser) - if err != nil { - return nil, err - } - return &updatedUser, nil -} - -type UpdateUserMetadata struct { - PublicMetadata json.RawMessage `json:"public_metadata"` - PrivateMetadata json.RawMessage `json:"private_metadata"` - UnsafeMetadata json.RawMessage `json:"unsafe_metadata"` -} - -func (s *UsersService) UpdateMetadata(userId string, updateMetadataRequest *UpdateUserMetadata) (*User, error) { - updateUserMetadataURL := fmt.Sprintf("%s/%s/metadata", UsersUrl, userId) - req, _ := s.client.NewRequest(http.MethodPatch, updateUserMetadataURL, updateMetadataRequest) - - var updatedUser User - _, err := s.client.Do(req, &updatedUser) - if err != nil { - return nil, err - } - return &updatedUser, nil -} - -func (s *UsersService) ListOAuthAccessTokens(userID, provider string) ([]*UserOAuthAccessToken, error) { - url := fmt.Sprintf("%s/%s/oauth_access_tokens/%s", UsersUrl, userID, provider) - req, _ := s.client.NewRequest(http.MethodGet, url) - - response := make([]*UserOAuthAccessToken, 0) - _, err := s.client.Do(req, &response) - if err != nil { - return nil, err - } - return response, nil -} - -type UserDisableMFAResponse struct { - UserID string `json:"user_id"` -} - -func (s *UsersService) DisableMFA(userID string) (*UserDisableMFAResponse, error) { - url := fmt.Sprintf("%s/%s/mfa", UsersUrl, userID) - req, _ := s.client.NewRequest(http.MethodDelete, url) - - var response UserDisableMFAResponse - if _, err := s.client.Do(req, &response); err != nil { - return nil, err - } - - return &response, nil -} - -func (s *UsersService) Ban(userID string) (*User, error) { - url := fmt.Sprintf("%s/%s/ban", UsersUrl, userID) - req, _ := s.client.NewRequest(http.MethodPost, url) - - var response User - if _, err := s.client.Do(req, &response); err != nil { - return nil, err - } - - return &response, nil -} - -func (s *UsersService) Unban(userID string) (*User, error) { - url := fmt.Sprintf("%s/%s/unban", UsersUrl, userID) - req, _ := s.client.NewRequest(http.MethodPost, url) - - var response User - if _, err := s.client.Do(req, &response); err != nil { - return nil, err - } - - return &response, nil -} - -func (s *UsersService) Lock(userID string) (*User, error) { - url := fmt.Sprintf("%s/%s/lock", UsersUrl, userID) - req, _ := s.client.NewRequest(http.MethodPost, url) - - var response User - if _, err := s.client.Do(req, &response); err != nil { - return nil, err - } - - return &response, nil -} - -func (s *UsersService) Unlock(userID string) (*User, error) { - url := fmt.Sprintf("%s/%s/unlock", UsersUrl, userID) - req, _ := s.client.NewRequest(http.MethodPost, url) - - var response User - if _, err := s.client.Do(req, &response); err != nil { - return nil, err - } - - return &response, nil -} - -type ListMembershipsParams struct { - Limit *int - Offset *int - UserID string -} - -func (s *UsersService) ListMemberships(params ListMembershipsParams) (*ListOrganizationMembershipsResponse, error) { - req, _ := s.client.NewRequest(http.MethodGet, fmt.Sprintf("%s/%s/organization_memberships", UsersUrl, params.UserID)) - - paginationParams := PaginationParams{Limit: params.Limit, Offset: params.Offset} - query := req.URL.Query() - addPaginationParams(query, paginationParams) - req.URL.RawQuery = query.Encode() - - var response ListOrganizationMembershipsResponse - if _, err := s.client.Do(req, &response); err != nil { - return nil, err - } - - return &response, nil -} diff --git a/clerk/users_test.go b/clerk/users_test.go deleted file mode 100644 index f24f1826..00000000 --- a/clerk/users_test.go +++ /dev/null @@ -1,597 +0,0 @@ -package clerk - -import ( - "encoding/json" - "fmt" - "net/http" - "net/url" - "reflect" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestUsersService_Create_happyPath(t *testing.T) { - token := "token" - var payload CreateUserParams - _ = json.Unmarshal([]byte(dummyCreateUserRequestJson), &payload) - - client, mux, _, teardown := setup(token) - defer teardown() - - mux.HandleFunc("/users", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, http.MethodPost) - testHeader(t, req, "Authorization", "Bearer "+token) - fmt.Fprint(w, dummyUserJson) - }) - - got, err := client.Users().Create(payload) - - var want User - _ = json.Unmarshal([]byte(dummyUserJson), &want) - - assert.Nil(t, err) - assert.Equal(t, want, *got) -} - -func TestUsersService_Create_invalidServer(t *testing.T) { - client, _ := NewClient("token") - - _, err := client.Users().Create(CreateUserParams{}) - if err == nil { - t.Errorf("Expected error to be returned") - } -} - -func TestUsersService_ListAll_happyPath(t *testing.T) { - client, mux, _, teardown := setup("token") - defer teardown() - - expectedResponse := "[" + dummyUserJson + "]" - - mux.HandleFunc("/users", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "GET") - testHeader(t, req, "Authorization", "Bearer token") - fmt.Fprint(w, expectedResponse) - }) - - var want []User - _ = json.Unmarshal([]byte(expectedResponse), &want) - - got, _ := client.Users().ListAll(ListAllUsersParams{}) - if len(got) != len(want) { - t.Errorf("Was expecting %d user to be returned, instead got %d", len(want), len(got)) - } - - if !reflect.DeepEqual(got, want) { - t.Errorf("Response = %v, want %v", got, want) - } -} - -func TestUsersService_ListAll_happyPathWithParameters(t *testing.T) { - client, mux, _, teardown := setup("token") - defer teardown() - - expectedResponse := "[" + dummyUserJson + "]" - - mux.HandleFunc("/users", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "GET") - testHeader(t, req, "Authorization", "Bearer token") - - actualQuery := req.URL.Query() - expectedQuery := url.Values(map[string][]string{ - "limit": {"5"}, - "offset": {"6"}, - "email_address": {"email1", "email2"}, - "phone_number": {"phone1", "phone2"}, - "web3_wallet": {"wallet1", "wallet2"}, - "username": {"username1", "username2"}, - "user_id": {"userid1", "userid2"}, - "query": {"my-query"}, - "last_active_at_since": {"1700690400000"}, - "order_by": {"created_at"}, - }) - assert.Equal(t, expectedQuery, actualQuery) - fmt.Fprint(w, expectedResponse) - }) - - var want []User - _ = json.Unmarshal([]byte(expectedResponse), &want) - - got, _ := client.Users().ListAll(ListAllUsersParams{ - Limit: intToPtr(5), - Offset: intToPtr(6), - EmailAddresses: []string{"email1", "email2"}, - PhoneNumbers: []string{"phone1", "phone2"}, - Web3Wallets: []string{"wallet1", "wallet2"}, - Usernames: []string{"username1", "username2"}, - UserIDs: []string{"userid1", "userid2"}, - Query: stringToPtr("my-query"), - LastActiveAtSince: int64ToPtr(int64(1700690400000)), - OrderBy: stringToPtr("created_at"), - }) - if len(got) != len(want) { - t.Errorf("Was expecting %d user to be returned, instead got %d", len(want), len(got)) - } - - if !reflect.DeepEqual(got, want) { - t.Errorf("Response = %v, want %v", got, want) - } -} - -func TestUsersService_ListAll_invalidServer(t *testing.T) { - client, _ := NewClient("token") - - users, err := client.Users().ListAll(ListAllUsersParams{}) - if err == nil { - t.Errorf("Expected error to be returned") - } - if users != nil { - t.Errorf("Was not expecting any users to be returned, instead got %v", users) - } -} - -func TestUsersService_Count_happyPath(t *testing.T) { - client, mux, _, teardown := setup("token") - defer teardown() - - expectedResponse := dummyUserCountJson - - mux.HandleFunc("/users/count", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "GET") - testHeader(t, req, "Authorization", "Bearer token") - fmt.Fprint(w, expectedResponse) - }) - - var want UserCount - _ = json.Unmarshal([]byte(expectedResponse), &want) - - got, _ := client.Users().Count(ListAllUsersParams{}) - if !reflect.DeepEqual(*got, want) { - t.Errorf("Response = %v, want %v", *got, want) - } -} - -func TestUsersService_Count_happyPathWithParameters(t *testing.T) { - client, mux, _, teardown := setup("token") - defer teardown() - - expectedResponse := dummyUserCountJson - - mux.HandleFunc("/users/count", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "GET") - testHeader(t, req, "Authorization", "Bearer token") - - actualQuery := req.URL.Query() - expectedQuery := url.Values(map[string][]string{ - "email_address": {"email1", "email2"}, - "phone_number": {"phone1", "phone2"}, - "web3_wallet": {"wallet1", "wallet2"}, - "username": {"username1", "username2"}, - "user_id": {"userid1", "userid2"}, - "query": {"my-query"}, - }) - assert.Equal(t, expectedQuery, actualQuery) - fmt.Fprint(w, expectedResponse) - }) - - var want UserCount - _ = json.Unmarshal([]byte(expectedResponse), &want) - - got, _ := client.Users().Count(ListAllUsersParams{ - EmailAddresses: []string{"email1", "email2"}, - PhoneNumbers: []string{"phone1", "phone2"}, - Web3Wallets: []string{"wallet1", "wallet2"}, - Usernames: []string{"username1", "username2"}, - UserIDs: []string{"userid1", "userid2"}, - Query: stringToPtr("my-query"), - }) - if !reflect.DeepEqual(*got, want) { - t.Errorf("Response = %v, want %v", got, want) - } -} - -func TestUsersService_Count_invalidServer(t *testing.T) { - client, _ := NewClient("token") - - users, err := client.Users().Count(ListAllUsersParams{}) - if err == nil { - t.Errorf("Expected error to be returned") - } - if users != nil { - t.Errorf("Was not expecting any users to be returned, instead got %v", users) - } -} - -func TestUsersService_Read_happyPath(t *testing.T) { - token := "token" - userId := "someUserId" - expectedResponse := dummyUserJson - - client, mux, _, teardown := setup(token) - defer teardown() - - mux.HandleFunc("/users/"+userId, func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "GET") - testHeader(t, req, "Authorization", "Bearer "+token) - fmt.Fprint(w, expectedResponse) - }) - - var want User - _ = json.Unmarshal([]byte(expectedResponse), &want) - - got, _ := client.Users().Read(userId) - if !reflect.DeepEqual(*got, want) { - t.Errorf("Response = %v, want %v", *got, want) - } -} - -func TestUsersService_Read_invalidServer(t *testing.T) { - client, _ := NewClient("token") - - user, err := client.Users().Read("someUserId") - if err == nil { - t.Errorf("Expected error to be returned") - } - if user != nil { - t.Errorf("Was not expecting any user to be returned, instead got %v", user) - } -} - -func TestUsersService_Delete_happyPath(t *testing.T) { - token := "token" - userId := "someUserId" - - client, mux, _, teardown := setup(token) - defer teardown() - - mux.HandleFunc("/users/"+userId, func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "DELETE") - testHeader(t, req, "Authorization", "Bearer "+token) - response := fmt.Sprintf(`{ "deleted": true, "id": "%v", "object": "user" }`, userId) - fmt.Fprint(w, response) - }) - - want := DeleteResponse{ID: userId, Object: "user", Deleted: true} - - got, _ := client.Users().Delete(userId) - if !reflect.DeepEqual(*got, want) { - t.Errorf("Response = %v, want %v", *got, want) - } -} - -func TestUsersService_Delete_invalidServer(t *testing.T) { - client, _ := NewClient("token") - - delResponse, err := client.Users().Delete("someUserId") - if err == nil { - t.Errorf("Expected error to be returned") - } - if delResponse != nil { - t.Errorf("Was not expecting any reponse to be returned, instead got %v", delResponse) - } -} - -func TestUsersService_Update_happyPath(t *testing.T) { - token := "token" - userId := "someUserId" - var payload UpdateUser - _ = json.Unmarshal([]byte(dummyUpdateRequestJson), &payload) - - client, mux, _, teardown := setup(token) - defer teardown() - - mux.HandleFunc("/users/"+userId, func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "PATCH") - testHeader(t, req, "Authorization", "Bearer "+token) - fmt.Fprint(w, dummyUserJson) - }) - - got, _ := client.Users().Update(userId, &payload) - - var want User - _ = json.Unmarshal([]byte(dummyUserJson), &want) - - if !reflect.DeepEqual(*got, want) { - t.Errorf("Response = %v, want %v", *got, payload) - } -} - -func TestUsersService_Update_invalidServer(t *testing.T) { - client, _ := NewClient("token") - - _, err := client.Users().Update("someUserId", nil) - if err == nil { - t.Errorf("Expected error to be returned") - } -} - -func TestUsersService_UpdateMetadata_happyPath(t *testing.T) { - token := "token" - userId := "someUserId" - var payload UpdateUserMetadata - _ = json.Unmarshal([]byte(dummyUpdateMetadataRequestJson), &payload) - - client, mux, _, teardown := setup(token) - defer teardown() - - mux.HandleFunc("/users/"+userId+"/metadata", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, http.MethodPatch) - testHeader(t, req, "Authorization", "Bearer "+token) - fmt.Fprint(w, dummyUserJson) - }) - - got, _ := client.Users().UpdateMetadata(userId, &payload) - - var want User - _ = json.Unmarshal([]byte(dummyUserJson), &want) - - if !reflect.DeepEqual(*got, want) { - t.Errorf("Response = %v, want %v", *got, payload) - } -} - -func TestUsersService_UpdateMetadata_invalidServer(t *testing.T) { - client, _ := NewClient("token") - - _, err := client.Users().UpdateMetadata("someUserId", nil) - if err == nil { - t.Errorf("Expected error to be returned") - } -} - -func TestUsersService_ListOAuthAccessTokens_happyPath(t *testing.T) { - token := "token" - userId := "someUserId" - provider := "testProvider" - - client, mux, _, teardown := setup(token) - defer teardown() - - mux.HandleFunc(fmt.Sprintf("/users/%s/oauth_access_tokens/%s", userId, provider), func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "GET") - testHeader(t, req, "Authorization", "Bearer "+token) - fmt.Fprint(w, dummyUserOAuthAccessTokensJson) - }) - - got, _ := client.Users().ListOAuthAccessTokens(userId, provider) - - want := make([]*UserOAuthAccessToken, 0) - _ = json.Unmarshal([]byte(dummyUserOAuthAccessTokensJson), &want) - - if !reflect.DeepEqual(got, want) { - t.Errorf("Response = %v, want %v", got, want) - } -} - -func TestUsersService_ListOAuthAccessTokens_invalidServer(t *testing.T) { - client, _ := NewClient("token") - - _, err := client.Users().ListOAuthAccessTokens("someUserId", "testProvider") - if err == nil { - t.Errorf("Expected error to be returned") - } -} - -func TestUsersService_DisableMFA_happyPath(t *testing.T) { - token := "token" - userID := "test-user-id" - var payload UpdateUserMetadata - _ = json.Unmarshal([]byte(dummyUpdateMetadataRequestJson), &payload) - - client, mux, _, teardown := setup(token) - defer teardown() - - mux.HandleFunc("/users/"+userID+"/mfa", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, http.MethodDelete) - testHeader(t, req, "Authorization", "Bearer "+token) - fmt.Fprint(w, `{"user_id":"`+userID+`"}`) - }) - - got, err := client.Users().DisableMFA(userID) - assert.NoError(t, err) - assert.Equal(t, userID, got.UserID) -} - -func TestUsersService_Ban_happyPath(t *testing.T) { - token := "token" - userID := "test-user-id" - - client, mux, _, teardown := setup(token) - defer teardown() - - mux.HandleFunc("/users/"+userID+"/ban", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, http.MethodPost) - testHeader(t, req, "Authorization", "Bearer "+token) - fmt.Fprint(w, dummyUserJson) - }) - - _, err := client.Users().Ban(userID) - assert.NoError(t, err) -} - -func TestUsersService_Unban_happyPath(t *testing.T) { - token := "token" - userID := "test-user-id" - - client, mux, _, teardown := setup(token) - defer teardown() - - mux.HandleFunc("/users/"+userID+"/unban", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, http.MethodPost) - testHeader(t, req, "Authorization", "Bearer "+token) - fmt.Fprint(w, dummyUserJson) - }) - - _, err := client.Users().Unban(userID) - assert.NoError(t, err) -} - -func TestUsersService_Lock_happyPath(t *testing.T) { - token := "token" - userID := "test-user-id" - - client, mux, _, teardown := setup(token) - defer teardown() - - mux.HandleFunc("/users/"+userID+"/lock", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, http.MethodPost) - testHeader(t, req, "Authorization", "Bearer "+token) - fmt.Fprint(w, dummyUserJson) - }) - - _, err := client.Users().Lock(userID) - assert.NoError(t, err) -} - -func TestUsersService_Unlock_happyPath(t *testing.T) { - token := "token" - userID := "test-user-id" - - client, mux, _, teardown := setup(token) - defer teardown() - - mux.HandleFunc("/users/"+userID+"/unlock", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, http.MethodPost) - testHeader(t, req, "Authorization", "Bearer "+token) - fmt.Fprint(w, dummyUserJson) - }) - - _, err := client.Users().Unlock(userID) - assert.NoError(t, err) -} - -const dummyUserJson = `{ - "birthday": "", - "created_at": 1610783813, - "email_addresses": [ - { - "email_address": "iron_man@avengers.com", - "id": "idn_1mebQ9KkZWrhbQGF8Yj", - "linked_to": [ - { - "id": "idn_1n8tzrjmoKzHtQkaFe1pvK1OqLr", - "type": "oauth_google" - } - ], - "object": "email_address", - "verification": { - "status": "verified", - "strategy": "from_oauth_google" - } - } - ], - "external_accounts": [ - { - "approved_scopes": "email https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile openid profile", - "email_address": "iron_man@avengers.com", - "family_name": "Stark", - "given_name": "Tony", - "google_id": "11031040442607", - "id": "idn_1mebQ8sPZOtb7UQgptk", - "object": "google_account", - "picture": "https://lh3.googleusercontent.com/a-/AOh14Gg-UlYe7Pzd8vngVKdFlNCuGTn7cqxx=s96-c" - } - ], - "first_name": "Anthony", - "gender": "", - "id": "user_1mebQggrD3xO5JfuHk7clQ94ysA", - "last_name": "Stark", - "object": "user", - "password_enabled": false, - "phone_numbers": [], - "primary_email_address_id": "idn_1n8tzqi8K5ydvb1K7RJEKjT7Wb8", - "primary_phone_number_id": null, - "profile_image_url": "https://lh3.googleusercontent.com/a-/AOh14Gg-UlYe7PzddYKJRu2r8vGTn7cqxx=s96-c", - "two_factor_enabled": false, - "updated_at": 1610783813, - "username": null, - "public_metadata": { - "address": { - "street": "Pennsylvania Avenue", - "number": "1600" - } - }, - "private_metadata": { - "app_id": 5 - }, - "last_sign_in_at": 1610783813000, - "banned": false, - "locked": false, - "lockout_expires_in_seconds": null, - "verification_attempts_remaining": null, - "last_active_at": 1700690400000 - }` - -const dummyUserOAuthAccessTokensJson = `[ - { - "object": "oauth_access_token", - "token": "test_token", - "provider": "oauth_testProvider", - "public_metadata": {}, - "label": null, - "scopes": [ - "user:read", - "user:write" - ] - } - ]` - -const dummyCreateUserRequestJson = `{ - "first_name": "Tony", - "last_name": "Stark", - "email_address": ["email@example.com"], - "phone_number": ["+30123456789"], - "password": "new_password", - "public_metadata": { - "address": { - "street": "Pennsylvania Avenue", - "number": "1600" - } - }, - "private_metadata": { - app_id: 5 - }, - "unsafe_metadata": { - viewed_profile: true - }, - "totp_secret": "AICJ3HCXKO4KOY6NDH6RII4E3ZYL5ZBH", - }` - -const dummyUpdateRequestJson = `{ - "first_name": "Tony", - "last_name": "Stark", - "primary_email_address_id": "some_image_id", - "primary_phone_number_id": "some_phone_id", - "profile_image": "some_profile_image", - "password": "new_password", - "public_metadata": { - "address": { - "street": "Pennsylvania Avenue", - "number": "1600" - } - }, - "private_metadata": { - app_id: 5 - }, - "unsafe_metadata": { - viewed_profile: true - }, - }` - -const dummyUpdateMetadataRequestJson = `{ - "public_metadata": { - "value": "public_value", - }, - "private_metadata": { - "contact_id": "some_contact_id", - }, - "unsafe_metadata": { - viewed_profile: true - }, - }` - -const dummyUserCountJson = `{ - "object": "total_count", - "total_count": 2 - }` diff --git a/clerk/verification.go b/clerk/verification.go deleted file mode 100644 index 2ec6a136..00000000 --- a/clerk/verification.go +++ /dev/null @@ -1,78 +0,0 @@ -package clerk - -import ( - "errors" - "net/http" -) - -const ( - CookieSession = "__session" - QueryParamSessionId = "_clerk_session_id" -) - -type VerificationService service - -type verifyRequest struct { - Token string `json:"token"` -} - -type Verification struct { - Status string `json:"status"` - Strategy string `json:"strategy"` - Attempts *int `json:"attempts"` - ExpireAt *int64 `json:"expire_at"` - VerifiedAtClient string `json:"verified_at_client,omitempty"` - - // needed for Web3 - Nonce *string `json:"nonce,omitempty"` - - // needed for OAuth - ExternalVerificationRedirectURL *string `json:"external_verification_redirect_url,omitempty"` - Error []byte `json:"error,omitempty"` -} - -func (s *VerificationService) Verify(req *http.Request) (*Session, error) { - if req == nil { - return nil, errors.New("cannot verify empty request") - } - cookie, err := req.Cookie(CookieSession) - if err != nil { - return nil, errors.New("couldn't find cookie " + CookieSession) - } - - sessionToken := cookie.Value - sessionId := req.URL.Query().Get(QueryParamSessionId) - - if sessionId == "" { - return s.useClientActiveSession(sessionToken) - } - - return s.client.Sessions().Verify(sessionId, sessionToken) -} - -func (s *VerificationService) useClientActiveSession(token string) (*Session, error) { - clientResponse, err := s.client.Clients().Verify(token) - if err != nil { - return nil, err - } - - if clientResponse.LastActiveSessionID == nil { - return nil, errors.New("no active sessions for given client") - } - - for _, session := range clientResponse.Sessions { - if session.ID == *clientResponse.LastActiveSessionID { - return session, nil - } - } - - return nil, errors.New("active session not included in client's sessions") -} - -func doVerify(client Client, url, token string, response interface{}) error { - tokenPayload := verifyRequest{Token: token} - req, _ := client.NewRequest("POST", url, &tokenPayload) - - _, err := client.Do(req, response) - return err -} diff --git a/clerk/verification_test.go b/clerk/verification_test.go deleted file mode 100644 index 1015bd84..00000000 --- a/clerk/verification_test.go +++ /dev/null @@ -1,224 +0,0 @@ -package clerk - -import ( - "encoding/json" - "fmt" - "net/http" - "net/url" - "reflect" - "testing" -) - -func TestVerificationService_Verify_useSessionId(t *testing.T) { - apiToken := "apiToken" - sessionId := "someSessionId" - sessionToken := "someSessionToken" - request := setupRequest(&sessionId, &sessionToken) - - client, mux, _, teardown := setup(apiToken) - defer teardown() - - expectedResponse := dummySessionJson - - mux.HandleFunc("/sessions/"+sessionId+"/verify", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "POST") - testHeader(t, req, "Authorization", "Bearer "+apiToken) - fmt.Fprint(w, expectedResponse) - }) - - got, err := client.Verification().Verify(request) - if err != nil { - t.Errorf("Was not expecting error to be returned, got %v instead", err) - } - - var want Session - _ = json.Unmarshal([]byte(expectedResponse), &want) - - if !reflect.DeepEqual(*got, want) { - t.Errorf("Response = %v, want %v", *got, want) - } -} - -func TestVerificationService_Verify_handleServerErrorWhenUsingSessionId(t *testing.T) { - sessionId := "someSessionId" - sessionToken := "someSessionToken" - request := setupRequest(&sessionId, &sessionToken) - - client, mux, _, teardown := setup("apiToken") - defer teardown() - - mux.HandleFunc("/sessions/"+sessionId+"/verify", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "POST") - w.WriteHeader(400) - }) - - _, err := client.Verification().Verify(request) - if err == nil { - t.Errorf("Was expecting error to be returned") - } -} - -func TestVerificationService_Verify_useClientActiveSession(t *testing.T) { - apiToken := "apiToken" - sessionToken := "someSessionToken" - request := setupRequest(nil, &sessionToken) - - client, mux, _, teardown := setup(apiToken) - defer teardown() - - clientResponseJson := dummyClientResponseJson - var clientResponse ClientResponse - _ = json.Unmarshal([]byte(clientResponseJson), &clientResponse) - - sessionJson := dummySessionJson - - mux.HandleFunc("/clients/verify", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "POST") - testHeader(t, req, "Authorization", "Bearer "+apiToken) - fmt.Fprint(w, clientResponseJson) - }) - - got, err := client.Verification().Verify(request) - if err != nil { - t.Errorf("Was not expecting error to be returned, got %v instead", err) - } - - var want Session - _ = json.Unmarshal([]byte(sessionJson), &want) - - if !reflect.DeepEqual(*got, want) { - t.Errorf("Response = %v, want %v", *got, want) - } -} - -func TestVerificationService_Verify_handleServerErrorWhenUsingClientActiveSession(t *testing.T) { - sessionToken := "someSessionToken" - request := setupRequest(nil, &sessionToken) - - client, mux, _, teardown := setup("apiToken") - defer teardown() - - mux.HandleFunc("/clients/verify", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "POST") - w.WriteHeader(400) - }) - - _, err := client.Verification().Verify(request) - if err == nil { - t.Errorf("Was expecting error to be returned") - } -} - -func TestVerificationService_Verify_noActiveSessionWhenUsingClientActiveSession(t *testing.T) { - apiToken := "apiToken" - sessionToken := "someSessionToken" - request := setupRequest(nil, &sessionToken) - - client, mux, _, teardown := setup(apiToken) - defer teardown() - - var clientResponse ClientResponse - _ = json.Unmarshal([]byte(dummyClientResponseJson), &clientResponse) - clientResponse.LastActiveSessionID = nil - - mux.HandleFunc("/clients/verify", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "POST") - jsonResp, _ := json.Marshal(clientResponse) - fmt.Fprint(w, string(jsonResp)) - }) - - _, err := client.Verification().Verify(request) - if err == nil { - t.Errorf("Was expecting error to be returned") - } -} - -func TestVerificationService_Verify_activeSessionNotIncludedInSessions(t *testing.T) { - apiToken := "apiToken" - sessionToken := "someSessionToken" - request := setupRequest(nil, &sessionToken) - - client, mux, _, teardown := setup(apiToken) - defer teardown() - - var clientResponse ClientResponse - _ = json.Unmarshal([]byte(dummyClientResponseJson), &clientResponse) - clientResponse.Sessions = make([]*Session, 0) - - mux.HandleFunc("/clients/verify", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "POST") - jsonResp, _ := json.Marshal(clientResponse) - fmt.Fprint(w, string(jsonResp)) - }) - - _, err := client.Verification().Verify(request) - if err == nil { - t.Errorf("Was expecting error to be returned") - } -} - -func TestVerificationService_Verify_notFailForEmptyRequest(t *testing.T) { - client, _, _, teardown := setup("apiToken") - defer teardown() - - session, err := client.Verification().Verify(nil) - if err == nil { - t.Errorf("Expected error to be returned") - } - - if session != nil { - t.Errorf("Expected no session to returned, got %v instead", session) - } -} - -func TestVerificationService_Verify_noSessionCookie(t *testing.T) { - client, _, _, teardown := setup("apiToken") - defer teardown() - - request := setupRequest(nil, nil) - - session, err := client.Verification().Verify(request) - if err == nil { - t.Errorf("Expected error to be returned") - } - - if session != nil { - t.Errorf("Expected no session to returned, got %v instead", session) - } -} - -func setupRequest(sessionId, sessionToken *string) *http.Request { - var request http.Request - request.Method = "GET" - - url := url.URL{ - Scheme: "http", - Host: "host.com", - Path: "/path", - } - request.URL = &url - - if sessionToken != nil { - // add session token as cookie - sessionCookie := http.Cookie{ - Name: CookieSession, - Value: *sessionToken, - } - request.Header = make(map[string][]string) - request.AddCookie(&sessionCookie) - } - - if sessionId != nil { - // add session id as query parameter - addQueryParam(&request, QueryParamSessionId, *sessionId) - } - - return &request -} - -func addQueryParam(req *http.Request, key, value string) { - url := req.URL - query := url.Query() - query.Add(key, value) - url.RawQuery = query.Encode() -} diff --git a/clerk/web3_wallets.go b/clerk/web3_wallets.go deleted file mode 100644 index 4ebfcf7b..00000000 --- a/clerk/web3_wallets.go +++ /dev/null @@ -1,8 +0,0 @@ -package clerk - -type Web3Wallet struct { - ID string `json:"id"` - Object string `json:"object"` - Web3Wallet string `json:"web3_wallet"` - Verification *Verification `json:"verification"` -} diff --git a/clerk/webhooks.go b/clerk/webhooks.go deleted file mode 100644 index a9fb08c1..00000000 --- a/clerk/webhooks.go +++ /dev/null @@ -1,38 +0,0 @@ -package clerk - -type WebhooksService service - -type SvixResponse struct { - SvixURL string `json:"svix_url"` -} - -func (s *WebhooksService) CreateSvix() (*SvixResponse, error) { - svixUrl := WebhooksUrl + "/svix" - req, _ := s.client.NewRequest("POST", svixUrl) - - var svixResponse SvixResponse - if _, err := s.client.Do(req, &svixResponse); err != nil { - return nil, err - } - return &svixResponse, nil -} - -func (s *WebhooksService) DeleteSvix() error { - svixUrl := WebhooksUrl + "/svix" - req, _ := s.client.NewRequest("DELETE", svixUrl) - - _, err := s.client.Do(req, nil) - - return err -} - -func (s *WebhooksService) RefreshSvixURL() (*SvixResponse, error) { - svixUrl := WebhooksUrl + "/svix_url" - req, _ := s.client.NewRequest("POST", svixUrl) - - var svixResponse SvixResponse - if _, err := s.client.Do(req, &svixResponse); err != nil { - return nil, err - } - return &svixResponse, nil -} diff --git a/clerk/webhooks_test.go b/clerk/webhooks_test.go deleted file mode 100644 index 453e0a49..00000000 --- a/clerk/webhooks_test.go +++ /dev/null @@ -1,104 +0,0 @@ -package clerk - -import ( - "encoding/json" - "fmt" - "net/http" - "reflect" - "testing" -) - -func TestWebhooksService_CreateSvix_happyPath(t *testing.T) { - client, mux, _, teardown := setup("token") - defer teardown() - - expectedResponse := dummySvixResponseJson - - mux.HandleFunc("/webhooks/svix", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "POST") - testHeader(t, req, "Authorization", "Bearer token") - fmt.Fprint(w, expectedResponse) - }) - - var want SvixResponse - _ = json.Unmarshal([]byte(expectedResponse), &want) - - got, _ := client.Webhooks().CreateSvix() - if !reflect.DeepEqual(*got, want) { - t.Errorf("response = %v, want %v", got, want) - } -} - -func TestWebhooksService_CreateSvix_invalidServer(t *testing.T) { - client, _ := NewClient("token") - - svixResponse, err := client.Webhooks().CreateSvix() - if err == nil { - t.Errorf("expected error to be returned") - } - if svixResponse != nil { - t.Errorf("was not expecting any users to be returned, instead got %v", svixResponse) - } -} - -func TestWebhooksService_DeleteSvix_happyPath(t *testing.T) { - client, mux, _, teardown := setup("token") - defer teardown() - - mux.HandleFunc("/webhooks/svix", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "DELETE") - testHeader(t, req, "Authorization", "Bearer token") - w.WriteHeader(http.StatusNoContent) - }) - - err := client.Webhooks().DeleteSvix() - if err != nil { - t.Errorf("was not expecting error, found %v instead", err) - } -} - -func TestWebhooksService_DeleteSvix_invalidServer(t *testing.T) { - client, _ := NewClient("token") - - err := client.Webhooks().DeleteSvix() - if err == nil { - t.Errorf("expected error to be returned") - } -} - -func TestWebhooksService_RefreshSvixURL_happyPath(t *testing.T) { - client, mux, _, teardown := setup("token") - defer teardown() - - expectedResponse := dummySvixResponseJson - - mux.HandleFunc("/webhooks/svix_url", func(w http.ResponseWriter, req *http.Request) { - testHttpMethod(t, req, "POST") - testHeader(t, req, "Authorization", "Bearer token") - fmt.Fprint(w, expectedResponse) - }) - - var want SvixResponse - _ = json.Unmarshal([]byte(expectedResponse), &want) - - got, _ := client.Webhooks().RefreshSvixURL() - if !reflect.DeepEqual(*got, want) { - t.Errorf("response = %v, want %v", got, want) - } -} - -func TestWebhooksService_RefreshSvixURL_invalidServer(t *testing.T) { - client, _ := NewClient("token") - - svixResponse, err := client.Webhooks().RefreshSvixURL() - if err == nil { - t.Errorf("expected error to be returned") - } - if svixResponse != nil { - t.Errorf("was not expecting any users to be returned, instead got %v", svixResponse) - } -} - -const dummySvixResponseJson = `{ - "svix_url": "http://example.svix.com" -}` diff --git a/clerk_test.go b/clerk_test.go new file mode 100644 index 00000000..75792f78 --- /dev/null +++ b/clerk_test.go @@ -0,0 +1,344 @@ +package clerk + +import ( + "bytes" + "context" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "strconv" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewAPIResponse(t *testing.T) { + body := []byte(`{"foo":"bar"}`) + resp := &http.Response{ + Status: "200 OK", + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewReader([]byte(`{}`))), + Header: http.Header(map[string][]string{ + "Clerk-Trace-Id": {"trace-id"}, + "x-custom-header": {"custom-header"}, + }), + } + res := NewAPIResponse(resp, body) + assert.Equal(t, body, []byte(res.RawJSON)) + assert.Equal(t, "200 OK", res.Status) + assert.Equal(t, http.StatusOK, res.StatusCode) + assert.Equal(t, "trace-id", res.TraceID) + assert.Equal(t, resp.Header, res.Header) +} + +func TestNewBackend(t *testing.T) { + withDefaults, ok := NewBackend(&BackendConfig{}).(*defaultBackend) + require.True(t, ok) + require.NotNil(t, withDefaults.HTTPClient) + assert.Equal(t, defaultHTTPTimeout, withDefaults.HTTPClient.Timeout) + assert.Equal(t, APIURL, withDefaults.URL) + + u := "https://some.other.url" + httpClient := &http.Client{} + config := &BackendConfig{ + URL: &u, + HTTPClient: httpClient, + } + withOverrides, ok := NewBackend(config).(*defaultBackend) + require.True(t, ok) + assert.Equal(t, u, withOverrides.URL) + assert.Equal(t, httpClient, withOverrides.HTTPClient) +} + +func TestGetBackend_DataRace(t *testing.T) { + wg := &sync.WaitGroup{} + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + defer wg.Done() + b, ok := GetBackend().(*defaultBackend) + require.True(t, ok) + assert.Equal(t, APIURL, b.URL) + }() + } + wg.Wait() +} + +func TestAPIErrorResponse(t *testing.T) { + // API error response that is valid JSON. The error + // string is the raw JSON. + resp := &APIErrorResponse{ + HTTPStatusCode: 200, + TraceID: "trace-id", + Errors: []Error{ + { + Code: "error-code", + Message: "message", + LongMessage: "long message", + }, + }, + } + expected := fmt.Sprintf(`{ + "status":%d, + "clerk_trace_id":"%s", + "errors":[{"code":"%s","message":"%s","long_message":"%s"}] +}`, + resp.HTTPStatusCode, + resp.TraceID, + resp.Errors[0].Code, + resp.Errors[0].Message, + resp.Errors[0].LongMessage, + ) + assert.JSONEq(t, expected, resp.Error()) +} + +// This is how you define a Clerk API resource which is ready to be +// used by the library. +type testResource struct { + APIResource + ID string `json:"id"` + Object string `json:"object"` +} + +// This is how you define types which can be used as Clerk API +// request parameters. +type testResourceParams struct { + APIParams + Name string `json:"name"` +} + +// This is how you define a Clerk API resource which can be used in +// API operations that read a list of resources. +type testResourceList struct { + APIResource + Resources []testResource `json:"data"` + TotalCount int64 `json:"total_count"` +} + +// This is how you define a type which can be used as parameters +// to a Clerk API operation that lists resources. +type testResourceListParams struct { + APIParams + ListParams + Name string `json:"name"` +} + +// We need to implement the Queryable interface. +func (params testResourceListParams) Add(q url.Values) { + q.Set("name", params.Name) + params.ListParams.Add(q) +} + +func TestBackendCall_RequestHeaders(t *testing.T) { + ctx := context.Background() + method := http.MethodPost + path := "/resources" + secretKey := "sk_test_123" + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, method, r.Method) + require.Equal(t, "/"+clerkAPIVersion+path, r.URL.Path) + + // The client sets the Authorization header correctly. + assert.Equal(t, fmt.Sprintf("Bearer %s", secretKey), r.Header.Get("Authorization")) + // The client sets the User-Agent header. + assert.Equal(t, "Clerk/v1 SDK-Go/v2.0.0", r.Header.Get("User-Agent")) + assert.Equal(t, "application/json", r.Header.Get("Content-Type")) + // The client includes a custom header with the SDK version. + assert.Equal(t, "go/v2.0.0", r.Header.Get("X-Clerk-SDK")) + + _, err := w.Write([]byte(`{}`)) + require.NoError(t, err) + })) + defer ts.Close() + + // Set up a mock backend which triggers requests to our test server above. + SetBackend(NewBackend(&BackendConfig{ + HTTPClient: ts.Client(), + URL: &ts.URL, + })) + + // Simulate usage for an API operation on a testResource. + // We need to initialize a request and use the Backend to send it. + SetKey(secretKey) + req := NewAPIRequest(method, path) + err := GetBackend().Call(ctx, req, &testResource{}) + require.NoError(t, err) +} + +// TestBackendCall_SuccessfulResponse_PostRequest tests that for POST +// requests (or other mutating operations) we serialize all parameters +// in the request body. +func TestBackendCall_SuccessfulResponse_PostRequest(t *testing.T) { + ctx := context.Background() + name := "the-name" + rawJSON := `{"id":"res_123","object":"resource"}` + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Assert that the request parameters were passed correctly in + // the request body. + body, err := io.ReadAll(r.Body) + require.NoError(t, err) + defer r.Body.Close() + assert.JSONEq(t, fmt.Sprintf(`{"name":"%s"}`, name), string(body)) + + _, err = w.Write([]byte(rawJSON)) + require.NoError(t, err) + })) + defer ts.Close() + + // Set up a mock backend which triggers requests to our test server above. + SetBackend(NewBackend(&BackendConfig{ + HTTPClient: ts.Client(), + URL: &ts.URL, + })) + + // Simulate usage for an API operation on a testResource. + // We need to initialize a request and use the Backend to send it. + resource := &testResource{} + req := NewAPIRequest(http.MethodPost, "/resources") + req.SetParams(&testResourceParams{Name: name}) + err := GetBackend().Call(ctx, req, resource) + require.NoError(t, err) + + // The API response has been unmarshaled in the testResource struct. + assert.Equal(t, "resource", resource.Object) + assert.Equal(t, "res_123", resource.ID) + // We stored the API response + require.NotNil(t, resource.Response) + assert.JSONEq(t, rawJSON, string(resource.Response.RawJSON)) +} + +// TestBackendCall_SuccessfulResponse_GetRequest tests that for GET +// requests which don't have a body, we serialize any parameters in +// the URL query string. +func TestBackendCall_SuccessfulResponse_GetRequest(t *testing.T) { + ctx := context.Background() + name := "the-name" + limit := 1 + rawJSON := `{"data": [{"id":"res_123","object":"resource"}], "total_count": 1}` + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Assert that the request parameters were set in the URL + // query string. + q := r.URL.Query() + assert.Equal(t, name, q.Get("name")) + assert.Equal(t, strconv.Itoa(limit), q.Get("limit")) + // Optional query parameters are omitted. + _, ok := q["offset"] + assert.False(t, ok) + + _, err := w.Write([]byte(rawJSON)) + require.NoError(t, err) + })) + defer ts.Close() + + // Set up a mock backend which triggers requests to our test server above. + SetBackend(NewBackend(&BackendConfig{ + HTTPClient: ts.Client(), + URL: &ts.URL, + })) + + // Simulate usage for an API operation on a testResourceList. + // We need to initialize a request and use the Backend to send it. + resource := &testResourceList{} + req := NewAPIRequest(http.MethodGet, "/resources") + req.SetParams(&testResourceListParams{ + ListParams: ListParams{ + Limit: Int64(int64(limit)), + }, + Name: name, + }) + err := GetBackend().Call(ctx, req, resource) + require.NoError(t, err) + + // The API response has been unmarshaled correctly into a list of + // testResource structs. + assert.Equal(t, "resource", resource.Resources[0].Object) + assert.Equal(t, "res_123", resource.Resources[0].ID) + // We stored the API response + require.NotNil(t, resource.Response) + assert.JSONEq(t, rawJSON, string(resource.Response.RawJSON)) +} + +// TestBackendCall_ParseableError tests responses with a non-successful +// status code and a body that can be deserialized to an "expected" +// error response. These errors usually happen due to a client error +// and result in 4xx response statuses. The Clerk API responds with a +// familiar response body. +func TestBackendCall_ParseableError(t *testing.T) { + errorJSON := `{ + "clerk_trace_id": "trace-id", + "errors": [ + { + "code": "error-code", + "message": "error-message", + "long_message": "long-error-message", + "meta": { + "param_name": "param-name" + } + } + ] +}` + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusUnprocessableEntity) + _, err := w.Write([]byte(errorJSON)) + require.NoError(t, err) + })) + defer ts.Close() + + SetBackend(NewBackend(&BackendConfig{ + HTTPClient: ts.Client(), + URL: &ts.URL, + })) + resource := &testResource{} + err := GetBackend().Call(context.Background(), NewAPIRequest(http.MethodPost, "/resources"), resource) + require.Error(t, err) + + // The error is an APIErrorResponse. We can assert on certain useful fields. + apiErr, ok := err.(*APIErrorResponse) + require.True(t, ok) + assert.Equal(t, http.StatusUnprocessableEntity, apiErr.HTTPStatusCode) + assert.Equal(t, "trace-id", apiErr.TraceID) + + // The response errors have been deserialized correctly. + require.Equal(t, 1, len(apiErr.Errors)) + assert.Equal(t, "error-code", apiErr.Errors[0].Code) + assert.Equal(t, "error-message", apiErr.Errors[0].Message) + assert.Equal(t, "long-error-message", apiErr.Errors[0].LongMessage) + assert.JSONEq(t, `{"param_name":"param-name"}`, string(apiErr.Errors[0].Meta)) + + // We've stored the raw response as well. + require.NotNil(t, apiErr.Response) + assert.JSONEq(t, errorJSON, string(apiErr.Response.RawJSON)) +} + +// TestBackendCall_ParseableError tests responses with a non-successful +// status code and a body that can be deserialized to an unexpected +// error response. This might happen when the Clerk API encounters an +// unexpected server error and usually results in 5xx status codes. +func TestBackendCall_NonParseableError(t *testing.T) { + errorResponse := `{invalid}` + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + _, err := w.Write([]byte(errorResponse)) + require.NoError(t, err) + })) + defer ts.Close() + + SetBackend(NewBackend(&BackendConfig{ + HTTPClient: ts.Client(), + URL: &ts.URL, + })) + resource := &testResource{} + err := GetBackend().Call(context.Background(), NewAPIRequest(http.MethodPost, "/resources"), resource) + require.Error(t, err) + // The raw error is returned since we cannot unmarshal it to a + // familiar API error response. + assert.Equal(t, errorResponse, err.Error()) +} diff --git a/docs/clerk-logo-dark.png b/docs/clerk-logo-dark.png deleted file mode 100644 index 1faeb9411dad12afcd0da917ef64b220278200c4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12638 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 M07*qoM6N<$g1b0y9RL6T diff --git a/docs/clerk-logo-light.png b/docs/clerk-logo-light.png deleted file mode 100644 index fe83c603236421132e8c7acedc843aa16f5390bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4812 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

  • u))v)rbir$<-uw|?^UDw-7@wfR+L8dtu3uKG3l_O;VO z{pT^aIJQ@eWI3wqyg1mWb2xsb&)>B19a%iZKH(}YqYY=7Bji~XhwHFtJ6v#npIQh@ zUpAqc=rs*nxHA)VUH{D`iR{z&jNB#JSN7p|z868mXj)$nruXsOrut-7AU~NjI;6p9 z^0oN0%HmF*qI{k-z!-cjSvQcNbyX^lI=is*yag`MC1&`5!PicZ`}shpGC@|Ya!{;~ zTg|XOi18?qpCo;k+6yH+wWHKbSs&}&u65)wAy9{B^pv6cq_mwAulA%jc>` zT2}gJ59P2avCHZ!FRAT(YMi|_291ez>BX;D>oXX~daO#{9~>#xD&KbOX@x~fb`E&o zxtLky@NrtsLhI4FQdd!%T!`;LV1l>&^Nd{|+7zLU7urJ;(L7(zvOTis)-G-Hc}%JA zt@&AHSF`-t7>~)+k@xKc=x3eeMx~>b*UKFmd7~Td4>Q6ljm$}-W=jqH>}(z23k9#A zJayNi?e|Qg@07^rZTFb#ph~7LPIM_Tb&=Pi=pOxRQC7BJ)n> z836-R{Kq>zw>VNs-xWgU9Y;*aW>CK2HX!kWQPLnJab~mWZvns+!IJ^B#90pgF>%J6 zINII3z5In1qU+U8+B2&p!HCTvWpSn0gqa8at>#hO6GB)6#M)XEK(Z553#ie_g}pqr z*8#>FU={E`WZac|y-aev{Mp@9e!78FpP=3`-B`ju!~0;z5{zb!E;uX`eiV9HLTKqU z*N0;KN(_Az1Zt_l?RD*S1o$!FMpa~Gcfp=n;X|O0?1vRWe^dVQuZ80?GTw8X!xxJ< zTi(2*q=w(VoNe`~*?GI|VsS$h7rO7IlWl20XOSaLh}ctXuUOfcuAedIp?Qs)iqyjT zxx`($v2>kjG54W6>w11p!|Y;-yH2@%@jzFG`DXgX_Vjy4yM>NLxra=5yP5NQx4y)F zwGY2Fy7~ZqJM>4B#!gU|Q~9^`X6nrHePu61`j&zBe<>#1$^=xjxns0b2alJozCFa>OMsNNbqx$fq zjPx!6=!miaic141>meBJhfLwyD(9hNyCL2)_vN^XZ&z_7Q>f{Fl;M{j ziqqjKUOgb{^ifN>7hLb(%U$K0&2kJGsFH2#C?nT_AQAwB$tM8(zx_`SaEmMH-@v+} z$u;m|2D!P_H1NO8ugGm78DW!-1|9UxsG;=LSZ0)YmJ5c2Xu8%B1YTbFTNrr|#Us`g zC781rm&ec(*O=LfvSdQ(MX2hl_3QPRD9zH3Sbbx$XCbQ1VB?yXC4x&AOZ3m&RoV2W zJqE&9qFY!r@&o{cPWpS5NjTq)E27N)R5nLig4xK}G%7TT??Eq?q^5agei@`eDNtd` zFAtwkW5LBf>>o!4=haz0#tBUd$!LLbp*d%e?YV5BPuQZk9`+lVoo;QI&t5Zh2@=Yf~0dTTJ3Q2I#7Y%{$Y|NS%_BEOhB%&1DmXTsUo7g3AM|Ei}NP^p$Sf zA$U;0m9wHfmejgeei0g7=FPAbzBcs?J{#+pCMcF2qOmK*zz7!k#&uz zX#h#Rp@(;EqR5bnhs%6byxQ_nA7{E7l6{2UGasW7Wpk#>I+Oi_X#SQ;azHa4nX1QZ zD%IVw6Z-wFeNh#?<%frvH_k=ae~V!x{o7d~+gGQQV$5RY@zLjnRks{^7GZWU-n1?R zPv**V>t}_g*sjGKhnY^5h9@|i>Wk&>krnjb0=agL&9xBx5QB8)U zcm6=e@YV(nyZW%t3ZE-9u8mDH_@{?hF|`Z+;dpm`F{)}y@tc)H8AFzmp zriKYYB1X%nOdUa`%&#LTP=ed7EezA+cODZ=e#Gg7pQiafu4?u|1--t8!51XvsIxNU z{K;RwRPx6;UNEYtWC*l3vllJpocDh195PgC%XUF1QeVn!@&0#5lm=xl`^Ili)kx?c zsQAJUSAr4+v6H7fux>-8#ENSA@i1Y`q^B@{oIxLCy#5s&C{U5ID&9I*863?m+R^TEOfk0;p}bBEz#`AcFIY3?C}0`hK4&PV^PS5Z%g|Z-mU3`PK3+7RP|!-$jM|mlIH8_O-W!TvV%a}3w3=zMba8E5+J%<#`7Q(GDpHs)dTf~!p?FB0Cq00L&#%&D4 zBCj*QpJax-KD}e$5m}b>IY<|#z~3x9;J;)0U^-$!%`UX`JdwaQEY_!5KHM1gy5!5J zl+JEjw;J8+E=PF&hnW0!gC~E2lwDSXB@+I^BQ;=^ur`Px%HXKGEs zY;AH4tr9pYK>RRx;z52NC*NTuy;G#W@MfU3lupIsP~VUw9Yx%4Uos&2-HSKp zGAgLDu7gwZneDQI$MSCWab#9v@HCk=tZV;NG|RNtwJnG%*@=d5-nqnIo?bPlU{$lX zUDQ*pRuY=x;KuPHekiR;{q;7ulDT&g^)c?k$$H7ic3{KvsKJ+0D0YgZpIVSdV}jF! zF?ct=&U-QXV5q=pTgX)Zl>KkPg4eXOjew^d_M2+n7AUClx%6IsJAFI50;lPkO@MruzHO--{SSC&aZYVBg0!!xJD1dNHu2XIQ!!Ie(?$S>eX1k zY{2$3Q_AeL_9-29}jm?$_+A zk$ZJNg7ZU7;H=?^!c9i?X|#_wTfNe)LD%EBXtqVf&4ZtdtBKjdJ8J!#2b;@9vink{ zGCK=>@`e3sMm~rgVaqWhorfW>gLaWO8y~qpy5CDEm}T*lXJJ5=S`=VvN#3I6U&;my zf>7Pq=ZVChUxbC92Ddvs7Ck&)FhQT-w95Ch@iI?Xa#duX1#3iUv?#dfz`u4*8@4YE z)xU*lso?F{hTUslQTn!2mFXMVpNzMXuW--yQ}4AiC6*+APRrshkWbreRPi%OnYFT@ zTS9yUiI^tIT5y(kpB4reh>k78(m(f)n6I(`gFA$C1lH2cP6SPg$IY)_PQBf^k_<}4 z1@WhX%Q+rpGPP=bh_A|jv^L9ujS9QMv@Iw`fwzSPXdCGy?r;hW6x2B?nEE;p0$gj& z0mEU6nEfyI{>=qgr_s_L)&8XByDr9a6-Nkyhm?h=-J2L@rcJSDj>iA6yR->!?{{AppName}} is the greatest app of all time!

    ", - FromEmailName: &fromEmailName, - }) - if err != nil { - t.Fatalf("Templates.Preview returned error: %v", err) - } - if templatePreview == nil { - t.Errorf("Templates.Preview returned nil") - } - } -} - -func TestTemplates_Upsert(t *testing.T) { - client := createClient() - - // Update one of the templates, just to make sure that the Upsert method works. - templateType := "email" - slug := "organization_invitation" - requiredVariable := "{{action_url}}" - deliveredByClerk := false - fromEmailName := "marketing" - upsertedTemplate, err := client.Templates().Upsert(templateType, slug, &clerk.UpsertTemplateRequest{ - Name: "Remarketing email", - Subject: "Unmissable opportunity", - Markup: "", - Body: fmt.Sprintf("Click %s for free unicorns", requiredVariable), - FromEmailName: &fromEmailName, - DeliveredByClerk: &deliveredByClerk, - }) - if err != nil { - t.Fatalf("Templates.Update returned error: %v", err) - } - if upsertedTemplate == nil { - t.Errorf("Templates.Upsert returned nil") - } -} diff --git a/tests/integration/users_test.go b/tests/integration/users_test.go deleted file mode 100644 index 0fe93326..00000000 --- a/tests/integration/users_test.go +++ /dev/null @@ -1,139 +0,0 @@ -//go:build integration -// +build integration - -package integration - -import ( - "encoding/json" - "testing" - - "github.com/clerkinc/clerk-sdk-go/clerk" - "github.com/stretchr/testify/assert" -) - -type addressDetails struct { - Street string `json:"street"` - Number string `json:"number"` -} - -type userAddress struct { - Address addressDetails `json:"address"` -} - -type userAppAndContactID struct { - AppID int `json:"app_id"` - ContactID int `json:"contact_id"` -} - -type userEvent struct { - ViewedProfile bool `json:"viewed_profile"` -} - -func TestUsers(t *testing.T) { - client := createClient() - - users, err := client.Users().ListAll(clerk.ListAllUsersParams{}) - if err != nil { - t.Fatalf("Users.ListAll returned error: %v", err) - } - if users == nil { - t.Fatalf("Users.ListAll returned nil") - } - - userCount, err := client.Users().Count(clerk.ListAllUsersParams{}) - if err != nil { - t.Fatalf("Users.Count returned error: %v", err) - } - if userCount.TotalCount == 0 { - t.Fatalf("Users.Count returned 0, expected %d", len(users)) - } - - for i, user := range users { - userId := user.ID - user, err := client.Users().Read(userId) - if err != nil { - t.Fatalf("Users.Read returned error: %v", err) - } - if user == nil { - t.Fatalf("Users.Read returned nil") - } - - updateRequest := clerk.UpdateUser{ - FirstName: user.FirstName, - LastName: user.LastName, - PublicMetadata: userAddress{Address: addressDetails{ - Street: "Fifth Avenue", - Number: "890", - }}, - PrivateMetadata: userAppAndContactID{AppID: i}, - UnsafeMetadata: userEvent{ - ViewedProfile: true, - }, - } - updatedUser, err := client.Users().Update(userId, &updateRequest) - if err != nil { - t.Fatalf("Users.Update returned error: %v", err) - } - if updatedUser == nil { - t.Errorf("Users.Update returned nil") - } - - privateMetadata := userAppAndContactID{ContactID: i} - privateMetadataJSON, _ := json.Marshal(privateMetadata) - - updatedUser, err = client.Users().UpdateMetadata(userId, &clerk.UpdateUserMetadata{ - PrivateMetadata: privateMetadataJSON, - }) - if err != nil { - t.Fatalf("Users.UpdateMetadata returned error: %v", err) - } - if updatedUser == nil { - t.Errorf("Users.UpdateMetadata returned nil") - } - - updatedUser, err = client.Users().Ban(userId) - if err != nil { - t.Fatalf("Users.Ban returned error: %v", err) - } - assert.True(t, updatedUser.Banned) - - updatedUser, err = client.Users().Unban(userId) - if err != nil { - t.Fatalf("Users.Unban returned error: %v", err) - } - assert.False(t, updatedUser.Banned) - - updatedUser, err = client.Users().Lock(userId) - if err != nil { - t.Fatalf("Users.Lock returned error: %v", err) - } - assert.True(t, updatedUser.Locked) - - updatedUser, err = client.Users().Unlock(userId) - if err != nil { - t.Fatalf("Users.Unlock returned error: %v", err) - } - assert.False(t, updatedUser.Locked) - } - - // Should return all memberships of a user - newOrganization, err := client.Organizations().Create(clerk.CreateOrganizationParams{ - Name: "my-org", - CreatedBy: users[0].ID, - }) - if err != nil { - t.Fatal(err) - } - - organizationMemberships, err := client.Users().ListMemberships(clerk.ListMembershipsParams{UserID: users[0].ID}) - assert.Equal(t, len(organizationMemberships.Data), 2) - assert.Equal(t, organizationMemberships.TotalCount, int64(2)) - assert.Equal(t, newOrganization.ID, organizationMemberships.Data[0].Organization.ID) - - // delete previous created organization to not create conflict with future tests - deleteResponse, err := client.Organizations().Delete(newOrganization.ID) - if err != nil { - t.Fatal(err) - } - assert.Equal(t, newOrganization.ID, deleteResponse.ID) -}