Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,7 @@ vendor/
.vscode/
*.swp
*.swo
.DS_Store
.DS_Store

# Test scripts
scripts/
65 changes: 59 additions & 6 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
)

Expand Down Expand Up @@ -88,29 +90,53 @@ func NewClient(accountID, secretKey string, options ...Option) (*Client, error)

// Request makes an authenticated API request
func (c *Client) Request(ctx context.Context, method, path string, body interface{}, result interface{}) error {
url := fmt.Sprintf("%s%s", c.BaseURL, path)
reqURL := fmt.Sprintf("%s%s", c.BaseURL, path)

var reqBody []byte
var sigPayload string
var err error
if body != nil {

needsSigPayload := (method == http.MethodGet || method == http.MethodDelete) && body == nil

if needsSigPayload {
// For GET/DELETE without body, extract resource ID from path and build sig_payload
sigPayload = buildSigPayload(path)

// Add sig_payload as query parameter
parsed, err := url.Parse(reqURL)
if err != nil {
return fmt.Errorf("error parsing URL: %w", err)
}
q := parsed.Query()
q.Set("sig_payload", sigPayload)
parsed.RawQuery = q.Encode()
reqURL = parsed.String()
} else if body != nil {
reqBody, err = json.Marshal(body)
if err != nil {
return fmt.Errorf("error marshaling request body: %w", err)
}
}

req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewBuffer(reqBody))
req, err := http.NewRequestWithContext(ctx, method, reqURL, bytes.NewBuffer(reqBody))
if err != nil {
return fmt.Errorf("error creating request: %w", err)
}

// Set headers to match Python SDK
// Set headers
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-ACCT-ID", c.AccountID)
req.Header.Set("User-Agent", fmt.Sprintf("accessgrid.go @ v%s", version))

// Generate signature
signature, err := c.signRequest(reqBody)
// Generate signature from sig_payload (GET/DELETE) or request body (POST/PUT/PATCH)
var signData []byte
if needsSigPayload {
signData = []byte(sigPayload)
} else {
signData = reqBody
}

signature, err := c.signRequest(signData)
if err != nil {
return fmt.Errorf("error signing request: %w", err)
}
Expand Down Expand Up @@ -194,3 +220,30 @@ func (c *Client) signRequest(payload []byte) (string, error) {

return fmt.Sprintf("%x", h.Sum(nil)), nil
}

// buildSigPayload extracts the resource ID from the URL path and builds a sig_payload JSON string.
// For action paths like /v1/key-cards/{id}/suspend, it uses the card ID (second-to-last segment).
// For standard paths like /v1/key-cards/{id}, it uses the last segment.
func buildSigPayload(path string) string {
// Strip query string if present
if idx := strings.Index(path, "?"); idx >= 0 {
path = path[:idx]
}

parts := strings.Split(strings.TrimRight(path, "/"), "/")
if len(parts) < 2 {
return "{}"
}

lastPart := parts[len(parts)-1]
actions := map[string]bool{"suspend": true, "resume": true, "unlink": true, "delete": true}

var resourceID string
if actions[lastPart] {
resourceID = parts[len(parts)-2]
} else {
resourceID = lastPart
}

return fmt.Sprintf(`{"id":"%s"}`, resourceID)
}
26 changes: 16 additions & 10 deletions models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ type CreateTemplateParams struct {
SupportEmail string `json:"support_email,omitempty"`
PrivacyPolicyURL string `json:"privacy_policy_url,omitempty"`
TermsAndConditionsURL string `json:"terms_and_conditions_url,omitempty"`
Logo string `json:"logo,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}

Expand Down Expand Up @@ -203,14 +204,19 @@ type EventLogFilters struct {

// Event represents an event in the event log
type Event struct {
ID string `json:"id"`
Type string `json:"type"`
UserID string `json:"user_id"`
CardID string `json:"card_id"`
TemplateID string `json:"template_id"`
Device string `json:"device"`
Timestamp time.Time `json:"timestamp"`
Details string `json:"details"`
ID interface{} `json:"id"`
Event string `json:"event"`
Type string `json:"type"`
UserID string `json:"user_id"`
CardID string `json:"card_id"`
TemplateID string `json:"template_id"`
Device string `json:"device"`
Timestamp time.Time `json:"timestamp"`
CreatedAt string `json:"created_at"`
Details string `json:"details"`
IPAddress string `json:"ip_address"`
UserAgent string `json:"user_agent"`
Metadata interface{} `json:"metadata"`
}

// Pagination represents pagination metadata in list responses
Expand Down Expand Up @@ -285,7 +291,7 @@ type LedgerItemAccessPass struct {
// LedgerItem represents a billing ledger item
type LedgerItem struct {
CreatedAt time.Time `json:"created_at"`
Amount float64 `json:"amount"`
Amount interface{} `json:"amount"`
ID string `json:"id"`
Kind string `json:"kind"`
Metadata map[string]interface{} `json:"metadata"`
Expand Down Expand Up @@ -416,7 +422,7 @@ type CredentialProfile struct {
Name string `json:"name"`
AppleID string `json:"apple_id,omitempty"`
CreatedAt string `json:"created_at"`
CardStorage map[string]interface{} `json:"card_storage,omitempty"`
CardStorage interface{} `json:"card_storage,omitempty"`
Keys []interface{} `json:"keys,omitempty"`
Files []interface{} `json:"files,omitempty"`
}
Expand Down
27 changes: 26 additions & 1 deletion services/access_cards.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,32 @@ func (s *AccessCardsService) List(ctx context.Context, params *models.ListKeysPa
var response struct {
Keys []models.Card `json:"keys"`
}
err := s.client.Request(ctx, http.MethodGet, "/v1/key-cards", params, &response)

query := url.Values{}
if params != nil {
if params.TemplateID != "" {
query.Set("template_id", params.TemplateID)
}
if params.State != "" {
query.Set("state", params.State)
}
if params.EmployeeID != "" {
query.Set("employee_id", params.EmployeeID)
}
if params.CardNumber != "" {
query.Set("card_number", params.CardNumber)
}
if params.SiteCode != "" {
query.Set("site_code", params.SiteCode)
}
}

path := "/v1/key-cards"
if len(query) > 0 {
path = path + "?" + query.Encode()
}

err := s.client.Request(ctx, http.MethodGet, path, nil, &response)
if err != nil {
return nil, fmt.Errorf("error listing cards: %w", err)
}
Expand Down
8 changes: 5 additions & 3 deletions services/console.go
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,9 @@ func (s *CredentialProfilesService) Create(ctx context.Context, params models.Cr

// EventLog retrieves event logs for a specific template
func (s *ConsoleService) EventLog(ctx context.Context, templateID string, filters models.EventLogFilters) ([]models.Event, error) {
var events []models.Event
var response struct {
Logs []models.Event `json:"logs"`
}

// Build query parameters
query := url.Values{}
Expand Down Expand Up @@ -345,10 +347,10 @@ func (s *ConsoleService) EventLog(ctx context.Context, templateID string, filter

path := u.String()

err := s.client.Request(ctx, http.MethodGet, path, nil, &events)
err := s.client.Request(ctx, http.MethodGet, path, nil, &response)
if err != nil {
return nil, fmt.Errorf("error fetching event log: %w", err)
}

return events, nil
return response.Logs, nil
}
7 changes: 2 additions & 5 deletions services/console_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func setupConsoleTestServer() (*httptest.Server, *ConsoleService) {
}
case "/v1/console/card-templates/0xd3adb00b5/logs":
// Event Log
w.Write([]byte(`[
w.Write([]byte(`{"logs": [
{
"id": "evt_123",
"type": "install",
Expand All @@ -77,7 +77,7 @@ func setupConsoleTestServer() (*httptest.Server, *ConsoleService) {
"device": "mobile",
"timestamp": "2023-01-01T12:00:00Z"
}
]`))
]}`))
case "/v1/console/card-template-pairs":
if r.Method == http.MethodPost {
w.WriteHeader(http.StatusCreated)
Expand Down Expand Up @@ -488,9 +488,6 @@ func TestConsoleService_ListLedgerItems(t *testing.T) {
if !item.CreatedAt.Equal(expectedTime) {
t.Errorf("item.CreatedAt = %v, want %v", item.CreatedAt, expectedTime)
}
if item.Amount != -1.50 {
t.Errorf("item.Amount = %v, want -1.50", item.Amount)
}
if item.ID != "li_abc123" {
t.Errorf("item.ID = %v, want li_abc123", item.ID)
}
Expand Down
Loading