Files
skybridge/user/client/client.go
2025-08-31 22:35:23 -04:00

270 lines
6.2 KiB
Go

package client
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"time"
"github.com/google/uuid"
"github.com/RyanCopley/skybridge/user/internal/domain"
)
// UserClient provides an interface to interact with the user service
type UserClient struct {
baseURL string
httpClient *http.Client
userEmail string // For authentication
}
// NewUserClient creates a new user service client
func NewUserClient(baseURL, userEmail string) *UserClient {
return &UserClient{
baseURL: baseURL,
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
userEmail: userEmail,
}
}
// NewUserClientWithTimeout creates a new user service client with custom timeout
func NewUserClientWithTimeout(baseURL, userEmail string, timeout time.Duration) *UserClient {
return &UserClient{
baseURL: baseURL,
httpClient: &http.Client{
Timeout: timeout,
},
userEmail: userEmail,
}
}
// CreateUser creates a new user
func (c *UserClient) CreateUser(req *domain.CreateUserRequest) (*domain.User, error) {
body, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
httpReq, err := c.newRequest("POST", "/api/users", bytes.NewBuffer(body))
if err != nil {
return nil, err
}
var user domain.User
err = c.doRequest(httpReq, &user)
if err != nil {
return nil, err
}
return &user, nil
}
// GetUserByID retrieves a user by ID
func (c *UserClient) GetUserByID(id uuid.UUID) (*domain.User, error) {
path := fmt.Sprintf("/api/users/%s", id.String())
httpReq, err := c.newRequest("GET", path, nil)
if err != nil {
return nil, err
}
var user domain.User
err = c.doRequest(httpReq, &user)
if err != nil {
return nil, err
}
return &user, nil
}
// GetUserByEmail retrieves a user by email
func (c *UserClient) GetUserByEmail(email string) (*domain.User, error) {
path := fmt.Sprintf("/api/users/email/%s", url.PathEscape(email))
httpReq, err := c.newRequest("GET", path, nil)
if err != nil {
return nil, err
}
var user domain.User
err = c.doRequest(httpReq, &user)
if err != nil {
return nil, err
}
return &user, nil
}
// UpdateUser updates an existing user
func (c *UserClient) UpdateUser(id uuid.UUID, req *domain.UpdateUserRequest) (*domain.User, error) {
body, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
path := fmt.Sprintf("/api/users/%s", id.String())
httpReq, err := c.newRequest("PUT", path, bytes.NewBuffer(body))
if err != nil {
return nil, err
}
var user domain.User
err = c.doRequest(httpReq, &user)
if err != nil {
return nil, err
}
return &user, nil
}
// DeleteUser deletes a user by ID
func (c *UserClient) DeleteUser(id uuid.UUID) error {
path := fmt.Sprintf("/api/users/%s", id.String())
httpReq, err := c.newRequest("DELETE", path, nil)
if err != nil {
return err
}
return c.doRequest(httpReq, nil)
}
// ListUsers retrieves a list of users with optional filters
func (c *UserClient) ListUsers(req *domain.ListUsersRequest) (*domain.ListUsersResponse, error) {
// Build query parameters
params := url.Values{}
if req.Status != nil {
params.Set("status", string(*req.Status))
}
if req.Role != nil {
params.Set("role", string(*req.Role))
}
if req.Search != "" {
params.Set("search", req.Search)
}
if req.Limit > 0 {
params.Set("limit", strconv.Itoa(req.Limit))
}
if req.Offset > 0 {
params.Set("offset", strconv.Itoa(req.Offset))
}
if req.OrderBy != "" {
params.Set("order_by", req.OrderBy)
}
if req.OrderDir != "" {
params.Set("order_dir", req.OrderDir)
}
path := "/api/users"
if len(params) > 0 {
path += "?" + params.Encode()
}
httpReq, err := c.newRequest("GET", path, nil)
if err != nil {
return nil, err
}
var response domain.ListUsersResponse
err = c.doRequest(httpReq, &response)
if err != nil {
return nil, err
}
return &response, nil
}
// ExistsByEmail checks if a user exists with the given email
func (c *UserClient) ExistsByEmail(email string) (bool, error) {
path := fmt.Sprintf("/api/users/exists/%s", url.PathEscape(email))
httpReq, err := c.newRequest("GET", path, nil)
if err != nil {
return false, err
}
var response map[string]interface{}
err = c.doRequest(httpReq, &response)
if err != nil {
return false, err
}
exists, ok := response["exists"].(bool)
if !ok {
return false, fmt.Errorf("invalid response format")
}
return exists, nil
}
// Health checks the health of the user service
func (c *UserClient) Health() (map[string]interface{}, error) {
httpReq, err := c.newRequest("GET", "/health", nil)
if err != nil {
return nil, err
}
var response map[string]interface{}
err = c.doRequest(httpReq, &response)
if err != nil {
return nil, err
}
return response, nil
}
// newRequest creates a new HTTP request with authentication headers
func (c *UserClient) newRequest(method, path string, body io.Reader) (*http.Request, error) {
url := c.baseURL + path
req, err := http.NewRequest(method, url, body)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
// Add authentication header
if c.userEmail != "" {
req.Header.Set("X-User-Email", c.userEmail)
}
return req, nil
}
// doRequest executes an HTTP request and handles the response
func (c *UserClient) doRequest(req *http.Request, target interface{}) error {
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("failed to execute request: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read response body: %w", err)
}
if resp.StatusCode >= 400 {
var errorResponse map[string]interface{}
if json.Unmarshal(body, &errorResponse) == nil {
if errorMsg, ok := errorResponse["error"].(string); ok {
return fmt.Errorf("API error (status %d): %s", resp.StatusCode, errorMsg)
}
}
return fmt.Errorf("HTTP error (status %d): %s", resp.StatusCode, string(body))
}
// If target is nil, we don't need to unmarshal (e.g., for DELETE requests)
if target == nil {
return nil
}
if err := json.Unmarshal(body, target); err != nil {
return fmt.Errorf("failed to unmarshal response: %w", err)
}
return nil
}