client

package
v0.3.12 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 18, 2026 License: AGPL-3.0 Imports: 17 Imported by: 0

README

Keyline API Client

The Keyline API Client is a Go package that provides a convenient, type-safe way to interact with the Keyline API. It's designed to simplify building custom tools or integrations.

Overview

The client is built on three core components:

  1. Client - Main entry point that provides access to resource-specific clients
  2. Transport - Handles HTTP communication, request/response processing, and virtual server routing
  3. Resource Clients - Specialized clients for different API resources (e.g., ApplicationClient)

Installation

The client is part of the Keyline repository and can be used directly:

import "Keyline/client"

Quick Start

Basic Usage
package main

import (
	"Keyline/client"
	"Keyline/internal/handlers"
	"context"
	"fmt"
)

func main() {
	// Create a new client
	c := client.NewClient(
		"http://localhost:8081", // Base URL of Keyline API
		"my-virtual-server",     // Virtual server name
	)

	ctx := context.Background()

	// Create an application
	app, err := c.Application().Create(ctx, handlers.CreateApplicationRequestDto{
		Name:           "my-app",
		DisplayName:    "My Application",
		RedirectUris:   []string{"http://localhost:3000/callback"},
		PostLogoutUris: []string{"http://localhost:3000/logout"},
		Type:           "public",
	})
	if err != nil {
		panic(err)
	}

	fmt.Printf("Created application with ID: %s\n", app.Id)
}

Architecture

Transport Layer

The Transport handles low-level HTTP communication:

  • URL Construction: Automatically constructs full URLs with virtual server routing
  • Request Building: Creates properly formatted HTTP requests
  • Error Handling: Converts HTTP errors into Go errors
  • Customization: Supports custom HTTP clients and middleware via options
Virtual Server Routing

All API requests are automatically routed through the virtual server path:

Base URL: http://localhost:8081
Virtual Server: my-virtual-server
Endpoint: /applications

Result: http://localhost:8081/api/virtual-servers/my-virtual-server/applications

Client Options

The client supports several configuration options:

Custom HTTP Client

Provide your own http.Client for custom timeouts, TLS configuration, etc.:

import (
    "net/http"
    "time"
)

httpClient := &http.Client{
    Timeout: 30 * time.Second,
}

c := client.NewClient(
    "http://localhost:8081",
    "my-virtual-server",
    client.WithClient(httpClient),
)
Custom Round Tripper (Middleware)

Add authentication, logging, or other middleware:

// Authentication middleware
authMiddleware := func(next http.RoundTripper) http.RoundTripper {
    return roundTripperFunc(func(req *http.Request) (*http.Response, error) {
        // Add bearer token
        req.Header.Set("Authorization", "Bearer "+token)
        return next.RoundTrip(req)
    })
}

c := client.NewClient(
    "http://localhost:8081",
    "my-virtual-server",
    client.WithRoundTripper(authMiddleware),
)

// Helper type for function-based round trippers
type roundTripperFunc func(*http.Request) (*http.Response, error)

func (f roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) {
    return f(r)
}
Custom Base URL

Override the base URL at runtime:

c := client.NewClient(
    "http://localhost:8081",
    "my-virtual-server",
    client.WithBaseURL("https://api.example.com"),
)

Error Handling

The client returns structured errors that can be inspected:

app, err := c.Application().Create(ctx, createDto)
if err != nil {
    // Check for API errors
    if apiErr, ok := err.(client.ApiError); ok {
        fmt.Printf("API Error: %s (HTTP %d)\n", apiErr.Message, apiErr.Code)
        
        switch apiErr.Code {
        case 401:
            // Handle unauthorized
        case 403:
            // Handle forbidden
        case 404:
            // Handle not found
        default:
            // Handle other errors
        }
    } else {
        // Handle other types of errors (network, etc.)
        fmt.Printf("Error: %v\n", err)
    }
}

Complete Example with Authentication

Here's a complete example showing authentication and error handling:

package main

import (
    "context"
    "fmt"
    "net/http"
    
    "Keyline/client"
    "Keyline/internal/handlers"
)

func main() {
    // Create authenticated client
    token := "your-bearer-token"
    
    c := client.NewClient(
        "http://localhost:8081",
        "my-virtual-server",
        client.WithRoundTripper(authMiddleware(token)),
    )

    ctx := context.Background()

    // Create an application
    app, err := c.Application().Create(ctx, handlers.CreateApplicationRequestDto{
        Name:           "example-app",
        DisplayName:    "Example Application",
        RedirectUris:   []string{"http://localhost:3000/callback"},
        PostLogoutUris: []string{"http://localhost:3000/logout"},
        Type:           "public",
    })
    if err != nil {
        handleError(err)
        return
    }

    fmt.Printf("✓ Created application: %s (ID: %s)\n", app.Name, app.Id)

    // List all applications
    apps, err := c.Application().List(ctx, client.ListApplicationParams{
        Page: 1,
        Size: 10,
    })
    if err != nil {
        handleError(err)
        return
    }

    fmt.Printf("✓ Found %d applications\n", len(apps.Items))
    for _, app := range apps.Items {
        fmt.Printf("  - %s: %s\n", app.Name, app.DisplayName)
    }
}

// authMiddleware adds bearer token authentication (just a readme example, not for production use)
func authMiddleware(token string) client.TransportOptions {
    return client.WithRoundTripper(func(next http.RoundTripper) http.RoundTripper {
        return roundTripperFunc(func(req *http.Request) (*http.Response, error) {
            req.Header.Set("Authorization", "Bearer "+token)
            return next.RoundTrip(req)
        })
    })
}

type roundTripperFunc func(*http.Request) (*http.Response, error)

func (f roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) {
    return f(r)
}

func handleError(err error) {
    if apiErr, ok := err.(client.ApiError); ok {
        fmt.Printf("✗ API Error: %s (HTTP %d)\n", apiErr.Message, apiErr.Code)
    } else {
        fmt.Printf("✗ Error: %v\n", err)
    }
}

Future Extensions

The client is designed to be extensible. Additional resource clients can be added by:

  1. Creating a new interface in the client package
  2. Implementing the interface with a struct that uses the Transport
  3. Adding a method to the main Client interface to access the new resource client

Example structure for adding a User client:

type UserClient interface {
    Create(ctx context.Context, dto handlers.CreateUserRequestDto) (handlers.CreateUserResponseDto, error)
    Get(ctx context.Context, id uuid.UUID) (handlers.GetUserResponseDto, error)
    // ... other methods
}

// In client.go
func (c *client) User() UserClient {
    return NewUserClient(c.transport)
}

Best Practices

  1. Always use context: Pass a proper context for cancellation and timeout support
  2. Handle errors properly: Check for both API errors and network errors
  3. Reuse clients: Create one client instance and reuse it across requests
  4. Use authentication middleware: Add bearer tokens or basic auth via round trippers
  5. Test with the client: Use it in integration tests for realistic API interactions
  6. Configure timeouts: Set appropriate HTTP client timeouts for your use case

Package Structure

client/
├── README.md          # This file
├── client.go          # Main client interface
├── transport.go       # HTTP transport layer
├── application.go     # Application resource client
└── application_test.go # Unit tests

Contributing

When adding new resource clients:

  1. Create the interface in a new file (e.g., user.go)
  2. Implement all CRUD operations following the Application client pattern
  3. Add comprehensive unit tests using httptest
  4. Update the main Client interface to expose the new resource client
  5. Document the new client in this README

For questions or issues, please refer to the main project documentation or open an issue on GitHub.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrAuthorizationPending = errors.New("authorization_pending")
	ErrAccessDenied         = errors.New("access_denied")
	ErrExpiredToken         = errors.New("expired_token")
	ErrSlowDown             = errors.New("slow_down")
	ErrInvalidUserCode      = errors.New("invalid_user_code")
)

Functions

This section is empty.

Types

type ApiError

type ApiError struct {
	Message string `json:"message"`
	Code    int    `json:"code"`
}

func (ApiError) Error

func (e ApiError) Error() string

type Client

type Client interface {
	VirtualServer() VirtualServerClient
	User() UserClient
	Oidc() OidcClient
	Project() ProjectClient
}

func NewClient

func NewClient(baseUrl string, virtualServer string, opts ...TransportOptions) Client

type ListApplicationParams

type ListApplicationParams struct {
	Page int
	Size int
}

type ListRoleParams added in v0.3.10

type ListRoleParams struct {
	Page int
	Size int
}

type ListUserParams

type ListUserParams struct {
	Page int
	Size int
}

type ListUsersInRoleParams added in v0.3.11

type ListUsersInRoleParams struct {
	Page int
	Size int
}

type OIDCRoundTripper

type OIDCRoundTripper struct {
	// contains filtered or unexported fields
}

func NewOIDCRoundTripper

func NewOIDCRoundTripper(next http.RoundTripper, tokenSource oauth2.TokenSource) *OIDCRoundTripper

func (*OIDCRoundTripper) RoundTrip

func (rt *OIDCRoundTripper) RoundTrip(req *http.Request) (*http.Response, error)

type OidcClient

type OidcClient interface {
	BeginDeviceFlow(ctx context.Context, clientId string, scope string) (api.DeviceAuthorizationResponse, error)
	PollDeviceToken(ctx context.Context, clientId string, deviceCode string) (api.DeviceTokenResponse, error)
	PostActivate(ctx context.Context, userCode string) (loginToken string, err error)
	VerifyPassword(ctx context.Context, loginToken string, username string, password string) error
	FinishLogin(ctx context.Context, loginToken string) error
}

func NewOidcClient

func NewOidcClient(transport *Transport) OidcClient

type PatchVirtualServerInput added in v0.3.0

type PatchVirtualServerInput struct {
	DisplayName              *string `json:"displayName"`
	EnableRegistration       *bool   `json:"enableRegistration"`
	Require2fa               *bool   `json:"require2fa"`
	RequireEmailVerification *bool   `json:"requireEmailVerification"`
}

PatchVirtualServerInput holds the fields that can be patched on a virtual server.

type ProjectClient added in v0.3.0

type ProjectClient interface {
	Create(ctx context.Context, input api.CreateProjectRequestDto) (api.CreateProjectResponseDto, error)
	Get(ctx context.Context, slug string) (api.GetProjectResponseDto, error)
	Application(projectSlug string) ApplicationClient
	Role(projectSlug string) RoleClient
}

func NewProjectClient added in v0.3.0

func NewProjectClient(transport *Transport) ProjectClient

type RoleClient added in v0.3.10

func NewRoleClient added in v0.3.10

func NewRoleClient(transport *Transport, projectSlug string) RoleClient

type ServiceUserTokenSource added in v0.3.1

type ServiceUserTokenSource struct {
	KeylineURL    string
	VirtualServer string
	PrivKeyPEM    string
	Kid           string
	Username      string
	Application   string
	// contains filtered or unexported fields
}

ServiceUserTokenSource implements oauth2.TokenSource via Keyline's RFC 8693 token exchange, signing a short-lived JWT with a service user's Ed25519 private key.

func (*ServiceUserTokenSource) Token added in v0.3.1

func (s *ServiceUserTokenSource) Token() (*oauth2.Token, error)

type Transport

type Transport struct {
	// contains filtered or unexported fields
}

func NewTransport

func NewTransport(baseUrl string, virtualServer string, options ...TransportOptions) *Transport

func (*Transport) Do

func (t *Transport) Do(req *http.Request) (*http.Response, error)

func (*Transport) DoNoRedirect

func (t *Transport) DoNoRedirect(req *http.Request) (*http.Response, error)

DoNoRedirect executes the request without following redirects.

func (*Transport) DoRaw

func (t *Transport) DoRaw(req *http.Request) (*http.Response, error)

DoRaw executes the request and returns the raw response without status code checking.

func (*Transport) NewOidcRequest

func (t *Transport) NewOidcRequest(ctx context.Context, method string, endpoint string, body io.Reader) (*http.Request, error)

func (*Transport) NewRootRequest

func (t *Transport) NewRootRequest(ctx context.Context, method string, endpoint string, body io.Reader) (*http.Request, error)

func (*Transport) NewTenantRequest

func (t *Transport) NewTenantRequest(ctx context.Context, method string, endpoint string, body io.Reader) (*http.Request, error)

type TransportOptions

type TransportOptions func(*Transport)

func WithBaseURL

func WithBaseURL(baseURL string) TransportOptions

func WithClient

func WithClient(client *http.Client) TransportOptions

func WithOidc

func WithOidc(tokenSource oauth2.TokenSource) TransportOptions

func WithRoundTripper

func WithRoundTripper(roundTripperFactory func(next http.RoundTripper) http.RoundTripper) TransportOptions

type UserClient

type UserClient interface {
	List(ctx context.Context, params ListUserParams) (api.PagedUsersResponseDto, error)
	Get(ctx context.Context, id uuid.UUID) (api.GetUserByIdResponseDto, error)
	Patch(ctx context.Context, id uuid.UUID, dto api.PatchUserRequestDto) error
	CreateServiceUser(ctx context.Context, username string) (uuid.UUID, error)
	AssociateServiceUserPublicKey(ctx context.Context, serviceUserID uuid.UUID, publicKeyPEM string) (string, error)
}

func NewUserClient

func NewUserClient(transport *Transport) UserClient

type VirtualServerClient

func NewVirtualServerClient

func NewVirtualServerClient(transport *Transport) VirtualServerClient

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL