diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 3c82733..b74d77b 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -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"} diff --git a/README.md b/README.md index 509654d..56c058d 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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) @@ -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: diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index e027bc1..c4c2f88 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -20,8 +20,10 @@ config.cassette_library_dir = "spec/vcr_cassettes" config.hook_into :webmock config.configure_rspec_metadata! - config.filter_sensitive_data("") { ENV["NATIONBUILDER_CLIENT_ID"] } - config.filter_sensitive_data("") { 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("") { ENV["NATIONBUILDER_CLIENT_ID"] } if ENV["NATIONBUILDER_CLIENT_ID"] + config.filter_sensitive_data("") { ENV["NATIONBUILDER_CLIENT_SECRET"] } if ENV["NATIONBUILDER_CLIENT_SECRET"] config.filter_sensitive_data("") do |interaction| interaction.response.headers["Authorization"]&.first end