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
1 change: 1 addition & 0 deletions .beads/issues.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
{"id":"nb-13","title":"Enhance HTTP Error Context","description":"Add request method and path to error messages for improved debugging (e.g., 'Authentication failed for GET /people')","design":"Update error raising in http_client.rb handle_response method to include method and path context. See roadmap item #8.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-02T17:09:08.185816+07:00","updated_at":"2025-12-04T10:16:52.99755+07:00","closed_at":"2025-12-04T10:16:52.99755+07:00"}
{"id":"nb-14","title":"Centralize OAuth HTTP Calls","description":"Refactor OAuth module to use HttpClient instead of direct HTTP.post calls for consistent logging and error handling across all HTTP operations","design":"Replace Net::HTTP direct calls in oauth.rb with HttpClient calls. Ensure token exchange and refresh operations use centralized client. See roadmap item #9.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-02T17:09:09.633647+07:00","updated_at":"2025-12-04T10:25:59.122885+07:00","closed_at":"2025-12-04T10:25:59.122885+07:00"}
{"id":"nb-2","title":"Add Network Error Test Coverage","description":"Implement tests for timeout scenarios and network error handling to ensure resilience","design":"Add tests for Net::OpenTimeout, Net::ReadTimeout, connection refused scenarios. See roadmap item #13.","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-02T17:09:16.462163+07:00","updated_at":"2025-12-02T17:26:17.183337+07:00"}
{"id":"nb-2o7","title":"Remove ENV var requirement for NationBuilder credentials","description":"Update initializer to not set global credentials, remove ENV vars from test_helper. Credentials should only be passed via Client.new parameters.","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-10T18:48:01.949364+07:00","updated_at":"2025-12-10T18:51:02.016399+07:00","closed_at":"2025-12-10T18:51:02.016399+07:00","external_ref":"https://github.com/ebrett/nationbuilder-client-v2/issues/7"}
{"id":"nb-3","title":"Add Redis \u0026 ActiveRecord Adapter Tests","description":"Implement comprehensive test suites for Redis and ActiveRecord token storage adapters to match Memory adapter test coverage. This addresses 3 failing ActiveRecord tests found in verification.","design":"Add test files spec/nationbuilder_api/token_storage/redis_spec.rb and enhance active_record_spec.rb. Include store_token, retrieve_token, delete_token, serialization tests. See roadmap item #7.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-02T17:09:06.202895+07:00","updated_at":"2025-12-02T17:32:40.986334+07:00","closed_at":"2025-12-02T17:32:40.986334+07:00"}
{"id":"nb-4","title":"Fix Token Adapter Validation","description":"Correct validate_adapter_interface! method in client.rb to use respond_to? instead of instance_methods set difference for accurate interface validation","design":"Change from instance_methods set comparison to respond_to? checks for each required method (get_token, store_token, delete_token). See roadmap item #6.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-02T17:09:04.540968+07:00","updated_at":"2025-12-02T17:26:17.184025+07:00","closed_at":"2025-12-02T17:18:07.625684+07:00"}
{"id":"nb-5","title":"Response Object Pattern","description":"Wrap API responses in typed objects instead of raw hashes for better developer experience and type safety (e.g., Person, Donation objects)","design":"Create response object classes in lib/nationbuilder_api/response_objects/. Use method_missing or define_method for attribute access. See roadmap item #18.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-02T17:09:38.473982+07:00","updated_at":"2025-12-02T17:26:17.184279+07:00"}
Expand Down
182 changes: 127 additions & 55 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,29 +37,38 @@ gem install nationbuilder-client-v2

## Quick Start

### Rails Application
### Basic Usage (Recommended)

```ruby
# config/initializers/nationbuilder_api.rb
NationbuilderApi.configure do |config|
config.client_id = ENV['NATIONBUILDER_CLIENT_ID']
config.client_secret = ENV['NATIONBUILDER_CLIENT_SECRET']
config.redirect_uri = 'https://your-app.com/oauth/callback'
end
```

### Non-Rails Application
Pass credentials directly when creating a client instance. This is the recommended approach, especially for multi-tenant applications:

```ruby
require 'nationbuilder_api'

client = NationbuilderApi::Client.new(
client_id: ENV['NATIONBUILDER_CLIENT_ID'],
client_secret: ENV['NATIONBUILDER_CLIENT_SECRET'],
client_id: 'your_client_id',
client_secret: 'your_client_secret',
redirect_uri: 'https://your-app.com/oauth/callback'
)
```

### Global Configuration (Optional)

For single-tenant applications, you can optionally set global defaults:

```ruby
# config/initializers/nationbuilder_api.rb (Rails)
NationbuilderApi.configure do |config|
config.client_id = ENV['NATIONBUILDER_CLIENT_ID']
config.client_secret = ENV['NATIONBUILDER_CLIENT_SECRET']
config.redirect_uri = ENV['NATIONBUILDER_REDIRECT_URI']
end

# Then create clients without passing credentials
client = NationbuilderApi::Client.new
```

**Note**: Global configuration is optional. Instance options always override global settings, making it easy to support multiple NationBuilder accounts.

## OAuth Authentication Flow

### 1. Generate Authorization URL
Expand Down Expand Up @@ -164,34 +173,123 @@ activities = client.people.activities(123)

## Configuration Options

### Global Configuration
### Instance Configuration (Recommended)

Pass configuration options directly when creating a client. This is the recommended approach for most applications:

```ruby
client = NationbuilderApi::Client.new(
# OAuth credentials (required)
client_id: 'your_client_id',
client_secret: 'your_client_secret',
redirect_uri: 'https://example.com/callback',

# Optional configuration
base_url: 'https://api.nationbuilder.com/v2', # Default
token_adapter: :active_record, # :memory, :redis, or custom adapter
timeout: 30, # HTTP timeout in seconds
identifier: 'account_123' # For multi-tenant applications
)
```

This approach allows you to:
- Manage multiple NationBuilder accounts with different credentials
- Store credentials in your database instead of environment variables
- Create clients with different configurations in the same application

### Global Configuration (Optional)

For single-tenant applications, you can optionally set global defaults that apply to all clients:

```ruby
NationbuilderApi.configure do |config|
# Required
config.client_id = 'your_client_id'
config.client_secret = 'your_client_secret'
config.redirect_uri = 'https://example.com/callback'

# Optional
config.base_url = 'https://api.nationbuilder.com/v2' # Default
config.token_adapter = :active_record # :memory, :redis, or custom adapter
config.timeout = 30 # HTTP timeout in seconds
# These are defaults - instance options will override them
config.base_url = 'https://api.nationbuilder.com/v2'
config.token_adapter = :active_record
config.timeout = 30
config.log_level = :info # :debug, :info, :warn, :error
end

# Create client - can override any global setting
client = NationbuilderApi::Client.new(
client_id: 'your_client_id',
client_secret: 'your_client_secret',
redirect_uri: 'https://example.com/callback',
timeout: 60 # Override global timeout
)
```

### Instance Configuration
**Important**: Credentials (`client_id`, `client_secret`, `redirect_uri`) are **not required** in global configuration. You can pass them per-instance, which is especially useful for multi-tenant applications.

## Multi-Tenant Usage

The gem is designed to support multi-tenant applications where you manage multiple NationBuilder accounts, each with their own OAuth credentials. This is the recommended pattern for SaaS applications.

### Basic Multi-Tenant Pattern

```ruby
# Instance configuration overrides global configuration
client = NationbuilderApi::Client.new(
client_id: 'custom_client_id',
timeout: 60,
token_adapter: :redis
# Account 1
client1 = NationbuilderApi::Client.new(
client_id: 'account1_client_id',
client_secret: 'account1_secret',
redirect_uri: 'https://example.com/callback',
identifier: 'account_1'
)

# Account 2
client2 = NationbuilderApi::Client.new(
client_id: 'account2_client_id',
client_secret: 'account2_secret',
redirect_uri: 'https://example.com/callback',
identifier: 'account_2'
)

# Each client uses separate credentials and tokens
client1.get('/people') # Uses account_1 credentials and token
client2.get('/people') # Uses account_2 credentials and token
```

### Storing Credentials in Database (Recommended)

For production multi-tenant applications, store OAuth credentials in your database:

```ruby
# app/models/nation.rb
class Nation < ApplicationRecord
# Columns: client_id, client_secret, redirect_uri, base_url
end

# app/services/nationbuilder_service.rb
class NationbuilderService
def initialize(nation)
@nation = nation
end

def client
@client ||= NationbuilderApi::Client.new(
client_id: @nation.client_id,
client_secret: @nation.client_secret,
redirect_uri: @nation.redirect_uri,
base_url: @nation.base_url,
identifier: "nation_#{@nation.id}",
token_adapter: :active_record
)
end
end

# Usage
nation = Nation.find(params[:nation_id])
service = NationbuilderService.new(nation)
people = service.client.people.show(123)
```

**Benefits of this approach:**
- No ENV variables required
- Each account has isolated credentials
- Credentials can be updated through your application UI
- Easy to add/remove accounts dynamically
- Better security through database encryption

## Token Storage Adapters

### Memory Adapter (Default for non-Rails)
Expand Down Expand Up @@ -308,32 +406,6 @@ end
- `ServerError` - 5xx server errors (retryable)
- `NetworkError` - Timeouts, connection failures (retryable)

## Multi-Tenant Usage

Manage multiple NationBuilder accounts using identifiers:

```ruby
# Account 1
client1 = NationbuilderApi::Client.new(
client_id: 'client_id',
client_secret: 'client_secret',
redirect_uri: 'https://example.com/callback',
identifier: 'account_1'
)

# Account 2
client2 = NationbuilderApi::Client.new(
client_id: 'client_id',
client_secret: 'client_secret',
redirect_uri: 'https://example.com/callback',
identifier: 'account_2'
)

# Each client uses separate tokens
client1.get('/people') # Uses account_1 token
client2.get('/people') # Uses account_2 token
```

## Logging

Logs automatically sanitize credentials:
Expand Down
6 changes: 4 additions & 2 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@
config.cassette_library_dir = "spec/vcr_cassettes"
config.hook_into :webmock
config.configure_rspec_metadata!
config.filter_sensitive_data("<NATIONBUILDER_CLIENT_ID>") { ENV["NATIONBUILDER_CLIENT_ID"] }
config.filter_sensitive_data("<NATIONBUILDER_CLIENT_SECRET>") { ENV["NATIONBUILDER_CLIENT_SECRET"] }
# Optional: Filter ENV vars from VCR cassettes if they're set
# These ENV vars are NOT required for tests to run
config.filter_sensitive_data("<NATIONBUILDER_CLIENT_ID>") { ENV["NATIONBUILDER_CLIENT_ID"] } if ENV["NATIONBUILDER_CLIENT_ID"]
config.filter_sensitive_data("<NATIONBUILDER_CLIENT_SECRET>") { ENV["NATIONBUILDER_CLIENT_SECRET"] } if ENV["NATIONBUILDER_CLIENT_SECRET"]
config.filter_sensitive_data("<ACCESS_TOKEN>") do |interaction|
interaction.response.headers["Authorization"]&.first
end
Expand Down