~tim/mailchimp3

b32428ccd5d2fc119fb236fd101b78bce1ac88df — Tim Morgan 6 years ago 95fdd76
Add support for OAuth2
M README.md => README.md +101 -21
@@ 11,40 11,120 @@ documented at [kb.mailchimp.com/api](http://kb.mailchimp.com/api/).
gem install mailchimp3 (eventually)
```

## Usage
## Usage with HTTP Basic Auth

1. Create a new API object, passing in your credentials (either HTTP Basic or OAuth2 access token):
1. Set your HTTP Basic auth key somewhere in your app
   (probably an initializer if using Rails):

    ```ruby
    # authenticate with HTTP Basic:
    api = MailChimp3.new(basic_auth_key: 'my key')
    # ...or authenticate with an OAuth2 access token (use the 'oauth2' gem to obtain the token)
    api = MailChimp3.new(oauth_access_token: 'token')
    ```
   ```ruby
   MailChimp3.config.basic_auth_key = 'key-us2'
   ```

2. Call a method on the api object to build the endpoint path.
2. Create a new API object and use it:

    ```ruby
    api.lists
    # /lists
    ```
   ```ruby
   # HTTP Basic
   api = MailChimp3.new
   ```

3. Call a method on the api object to build the endpoint path.

   ```ruby
   api.lists
   # /lists
   ```

4. For IDs, treat the object like a hash (use square brackets).

   ```ruby
   api.lists['abc123'].members
   # /lists/abc123/members
   ```

5. To execute the request, use `get`, `post`, `patch`, or `delete`, optionally passing arguments.

   ```ruby
   api.lists.get(count: 25)
   # GET /lists?count=25
   api.lists['abc123'].members['cde345'].get
   # GET /lists/abc123/members/cde345
   ```

### Usage with OAuth 2

1. Set your OAuth client id and secret somewhere in your app
   (probably an initializer if using Rails):

3. For IDs, treat the object like a hash (use square brackets).
   ```ruby
   MailChimp3.config.client_id = 'abc123'
   MailChimp3.config.client_secret = 'xyz456'
   ```

2. (First time for each user) Get an OAuth 2 token by calling `MailChimp3.oauth.authorize_url` and redirect your user to it:

   ```ruby
   url = MailChimp3.oauth.authorize_url(
     redirect_uri: 'http://example.com/oauth/callback'
   )
   redirect_to url
   ```

3. Upon redirect back to your app (in your `/oauth/callback` action),
   call `MailChimp3.oauth.complete_auth`, passing in the `code` param
   and the `redirect_uri` again.

   ```ruby
   data = MailChimp3.oauth.complete_auth(
     params[:code],
     redirect_uri: 'http://example.com/oauth/callback'
   )
   ```

   The hash returned looks like this:

   ```ruby
   {
     token: <OAuth2::AccessToken>
     token_string: 'abc123',
     metadata: {
       dc: 'us2'
     }
   }
   ```

   Then get the authentication token and data center, and store it on
   your user record for later use:

   ```ruby
   user.update_attributes(
     mailchimp_auth_token: data[:token_string],
     mailchimp_data_center: data[:metadata][:dc]
   )
   ```

4. (Subsequent times for user) Instantiate the api object, passing in the
   auth token and data center:

    **Option 1**: Pass in the user object, assuming it has the following methods:

    * `mailchimp_auth_token` (returns the oauth2 token string)
    * `mailchimp_data_center` (returns the mailchimp data center string, e.g. `us2`)

    ```ruby
    api.lists['abc123'].members
    # /lists/abc123/members
    api = MailChimp3.new(user)
    ```

4. To execute the request, use `get`, `post`, `patch`, or `delete`, optionally passing arguments.
    **Option 2**: Pass in the individual components as named arguments:

    ```ruby
    api.lists.get(count: 25)
    # GET /lists?count=25
    api.lists['abc123'].members['cde345'].get
    # GET /lists/abc123/members/cde345
    api = MailChimp3.new(
      auth_token: user.mailchimp_auth_token,
      data_center: user.mailchimp_data_center
    )
    ```

4. Use the `api` instance to make API calls!

## Example

```ruby

M lib/mailchimp3.rb => lib/mailchimp3.rb +10 -0
@@ 1,9 1,19 @@
require_relative 'mailchimp3/endpoint'
require_relative 'mailchimp3/oauth'
require_relative 'mailchimp3/errors'

module MailChimp3
  module_function

  def new(*args)
    Endpoint.new(*args)
  end

  def config
    @config ||= Struct.new(:client_id, :client_secret).new
  end

  def oauth
    @oauth ||= MailChimp3::OAuth.new
  end
end

M lib/mailchimp3/endpoint.rb => lib/mailchimp3/endpoint.rb +13 -11
@@ 5,11 5,13 @@ module MailChimp3
  class Endpoint
    attr_reader :url, :last_result

    def initialize(oauth_access_token: nil, basic_auth_key: nil, connection: nil, url: nil)
    def initialize(oauth_access_token: nil, basic_auth_key: nil, dc: nil, url: nil)
      @oauth_access_token = oauth_access_token
      @basic_auth_key = basic_auth_key
      @dc = dc
      @dc ||= @basic_auth_key.split('-').last if @basic_auth_key
      @url = url || _build_url
      @connection = connection || _build_connection
      fail Errors::DataCenterRequiredError, 'You must pass dc.' unless @dc || @url
      @cache = {}
    end



@@ 33,26 35,26 @@ module MailChimp3
    end

    def get(params = {})
      @last_result = @connection.get(@url, params)
      @last_result = _connection.get(@url, params)
      _build_response(@last_result)
    end

    def post(body = {})
      @last_result = @connection.post(@url) do |req|
      @last_result = _connection.post(@url) do |req|
        req.body = _build_body(body)
      end
      _build_response(@last_result)
    end

    def patch(body = {})
      @last_result = @connection.patch(@url) do |req|
      @last_result = _connection.patch(@url) do |req|
        req.body = _build_body(body)
      end
      _build_response(@last_result)
    end

    def delete
      @last_result = @connection.delete(@url)
      @last_result = _connection.delete(@url)
      if @last_result.status == 204
        true
      else


@@ 105,18 107,18 @@ module MailChimp3
      @cache[path] ||= begin
        self.class.new(
          url: File.join(url, path.to_s),
          connection: @connection
          basic_auth_key: @basic_auth_key,
          oauth_access_token: @oauth_access_token
        )
      end
    end

    def _build_url
      data_center = @basic_auth_key.split('-').last
      "https://#{data_center}.api.mailchimp.com/3.0"
      "https://#{@dc}.api.mailchimp.com/3.0"
    end

    def _build_connection
      Faraday.new(url: url) do |faraday|
    def _connection
      @connection ||= Faraday.new(url: url) do |faraday|
        faraday.adapter :excon
        faraday.response :json, content_type: /\bjson$/
        if @basic_auth_key

M lib/mailchimp3/errors.rb => lib/mailchimp3/errors.rb +1 -0
@@ 1,6 1,7 @@
module MailChimp3
  module Errors
    class AuthRequiredError < StandardError; end
    class DataCenterRequiredError < StandardError; end

    class BaseError < StandardError
      attr_reader :status, :details

A lib/mailchimp3/oauth.rb => lib/mailchimp3/oauth.rb +42 -0
@@ 0,0 1,42 @@
require 'oauth2'
require 'json'

module MailChimp3
  class OAuth
    def initialize
      @oauth = OAuth2::Client.new(
        MailChimp3.config.client_id,
        MailChimp3.config.client_secret,
        site: 'https://login.mailchimp.com',
        authorize_url: '/oauth2/authorize',
        token_url: '/oauth2/token'
      )
    end

    def authorize_url(redirect_uri:)
      @oauth.auth_code.authorize_url(redirect_uri: redirect_uri)
    end

    def complete_auth(code, redirect_uri:)
      token = @oauth.auth_code.get_token(
        code,
        redirect_uri: redirect_uri
      )
      {
        token: token,
        token_string: token.token,
        metadata: metadata(token)
      }
    end

    private

    def metadata(token)
      JSON.parse(token.get('/oauth2/metadata').body).tap do |hash|
        hash.keys.each do |key|
          hash[key.to_sym] = hash.delete(key)
        end
      end
    end
  end
end

M mailchimp3.gemspec => mailchimp3.gemspec +1 -0
@@ 20,6 20,7 @@ Gem::Specification.new do |s|
  s.add_dependency "faraday", "~> 0.9.1"
  s.add_dependency "faraday_middleware", "~> 0.9.1"
  s.add_dependency "excon", "~> 0.45.3"
  s.add_dependency "oauth2", "~> 1.0.0"
  s.add_development_dependency "rspec", "~> 3.2"
  s.add_development_dependency "webmock", "~> 1.21"
  s.add_development_dependency "pry", "~> 0.10"

A spec/mailchimp3/oauth_spec.rb => spec/mailchimp3/oauth_spec.rb +76 -0
@@ 0,0 1,76 @@
require_relative '../spec_helper'

MailChimp3.config.client_id = 'foo'
MailChimp3.config.client_secret = 'bar'

describe MailChimp3::OAuth do
  subject { MailChimp3.oauth }

  describe '#authorize_url' do
    it 'returns the authorization url' do
      url = subject.authorize_url(
        redirect_uri: 'http://example.com/oauth/callback'
      )
      expect(url).to eq('https://login.mailchimp.com/oauth2/authorize?client_id=foo&redirect_uri=http%3A%2F%2Fexample.com%2Foauth%2Fcallback&response_type=code')
    end
  end

  describe '#complete_auth' do
    before do
      stub_request(:post, 'https://login.mailchimp.com/oauth2/token')
        .with(body: {
          'client_id'     => 'foo',
          'client_secret' => 'bar',
          'code'          => '1234567890',
          'grant_type'    => 'authorization_code',
          'redirect_uri'  => 'http://example.com/oauth/callback'
        })
        .to_return(
          status: 200,
          body: {
            access_token: '925680f04933b28f128d721fdf8949fa',
            expires_in: 0,
            scope: nil
          }.to_json,
          headers: {
            'Content-Type' => 'application/json'
          }
        )
        stub_request(:get, 'https://login.mailchimp.com/oauth2/metadata')
          .with(headers: {
            'Authorization' => 'Bearer 925680f04933b28f128d721fdf8949fa'
          })
          .to_return(
            status: 200,
            body: {
              dc: 'us2',
              role: 'owner',
              accountname: 'timmorgan',
              user_id: 2472146,
              login: {
                email: 'tim@timmorgan.org',
                avatar: nil,
                login_id: 2472146,
                login_name: 'timmorgan',
                login_email:'tim@timmorgan.org'
              },
              login_url: 'https://login.mailchimp.com',
              api_endpoint: 'https://us2.api.mailchimp.com'
            }.to_json)
    end

    it 'stores the auth token and metadata' do
      url = subject.authorize_url(
        redirect_uri: 'http://example.com/oauth/callback'
      )
      hash = subject.complete_auth(
        '1234567890',
        redirect_uri: 'http://example.com/oauth/callback'
      )
      expect(hash[:token]).to be_an(OAuth2::AccessToken)
      expect(hash[:token_string]).to eq('925680f04933b28f128d721fdf8949fa')
      expect(hash[:metadata]).to be_a(Hash)
      expect(hash[:metadata][:dc]).to eq('us2')
    end
  end
end

A spec/mailchimp3_spec.rb => spec/mailchimp3_spec.rb +18 -0
@@ 0,0 1,18 @@
require_relative 'spec_helper'

describe MailChimp3 do
  describe '#config' do
    it 'returns configuration object' do
      subject.config.client_id = 'foo'
      subject.config.client_secret = 'bar'
      expect(subject.config.client_id).to eq('foo')
      expect(subject.config.client_secret).to eq('bar')
    end
  end

  describe '#oauth' do
    it 'returns a MailChimp3::OAuth instance' do
      expect(subject.oauth).to be_a(MailChimp3::OAuth)
    end
  end
end

M spec/spec_helper.rb => spec/spec_helper.rb +1 -0
@@ 1,4 1,5 @@
require 'webmock/rspec'
require 'pry'

require_relative '../lib/mailchimp3'