bunq-client 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 22487972e1aebeb56b45b8781968b99da449bc9a
4
- data.tar.gz: 2f1b2e7500ae78bf18a066d45b751e38cf56fbc8
2
+ SHA256:
3
+ metadata.gz: f43869cbdc6e1d596f972aac0752ca7ed25fe6884814ea592a7e34e97fae6948
4
+ data.tar.gz: 38a49c91d8e233921b8d21b1e5852e01acfff630157abe0dd3bfa4f715f4a10f
5
5
  SHA512:
6
- metadata.gz: 00f65e325aa9b546c8f1124c6debf626b07d21a546be70f8af5f901bcb8d74bcdff85b067775ed17c2dd103502d422499201904d5f2fc1673780a363f42546e4
7
- data.tar.gz: 29544093b45a56c096c45599263fcd29fccfd124a5e2a52515bc4d3eb8434ed0b59f5bba687d1f45ff63f88c68cae2768f8c727057a1ecc66f56df2f1503fb86
6
+ metadata.gz: a853e3ae7694c1bfb5a41aa735c4942ff015c8614602f6efd45d9a9aa08300ee1e89da4ad2ff3f354d82ef4ba519f10229badd1338dd9730d61ec8d8b2cb475d
7
+ data.tar.gz: ac8ee43d4bb61f30ce45937b7b90af82087cccd0d248396d27decdcbcdfae6584f4bf40f5c434135cc9ac7d248ebafdd9f44039557d59a433b8124ab1a06e4d2
@@ -1 +1,2 @@
1
- 2.3.2
1
+ 2.5.3
2
+
@@ -1,5 +1,5 @@
1
1
  sudo: false
2
2
  language: ruby
3
3
  rvm:
4
- - 2.3.2
5
- before_install: gem install bundler -v 1.13.6
4
+ - 2.3.4
5
+ before_install: gem install bundler -v 1.16.1
data/README.md CHANGED
@@ -137,6 +137,17 @@ Bunq.client.me_as_user.monetary_accounts.index.each do |monetary_account|
137
137
  end
138
138
  ```
139
139
 
140
+ ## Session caching
141
+
142
+ By default, each `Bunq.client` creates a new session. If you want to share a session between multiple
143
+ `Bunq.client`s, use the following configuration:
144
+
145
+ ```
146
+ Bunq.configure do |config|
147
+ config.session_cache = Bunq::ThreadSafeSessionCache.new
148
+ end
149
+ ```
150
+
140
151
  ## Development
141
152
 
142
153
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -10,11 +10,13 @@ Gem::Specification.new do |spec|
10
10
  'Lars Vonk',
11
11
  'Bob Forma',
12
12
  'Derek Kraan',
13
+ 'Mike van Diepen',
13
14
  ]
14
15
  spec.email = [
15
16
  'lars.vonk@gmail.com',
16
17
  'bforma@zilverline.com',
17
- 'dkraan@zilverline.com',
18
+ 'derek.kraan@gmail.com',
19
+ 'mvdiepen@zilverline.com',
18
20
  ]
19
21
 
20
22
  spec.summary = %q{Ruby client for the bunq public API.}
@@ -22,7 +24,6 @@ Gem::Specification.new do |spec|
22
24
  spec.homepage = "https://github.com/jorttbv/bunq-client"
23
25
  spec.license = "MIT"
24
26
 
25
-
26
27
  spec.files = `git ls-files -z`.split("\x0").reject do |f|
27
28
  f.match(%r{^(test|spec|features)/})
28
29
  end
@@ -31,11 +32,12 @@ Gem::Specification.new do |spec|
31
32
  spec.require_paths = ["lib"]
32
33
 
33
34
  spec.add_runtime_dependency 'rest-client', '~> 2.0'
35
+ spec.add_runtime_dependency 'thread_safe', '~> 0.3.6'
34
36
 
35
- spec.add_development_dependency "bundler", "~> 1.13"
36
- spec.add_development_dependency "rake", "~> 10.0"
37
- spec.add_development_dependency "rspec", "~> 3.5"
38
- spec.add_development_dependency "webmock", "~> 2.3.2"
37
+ spec.add_development_dependency "bundler", "~> 1.16"
38
+ spec.add_development_dependency "rake", "~> 12.3"
39
+ spec.add_development_dependency "rspec", "~> 3.7"
40
+ spec.add_development_dependency "webmock", "~> 3.2.1"
39
41
  spec.add_development_dependency "rspec-json_expectations", "~> 2.1"
40
42
  spec.add_development_dependency "codecov", "~> 0.1.10"
41
43
  end
@@ -0,0 +1,19 @@
1
+ module Bunq
2
+ ##
3
+ # https://doc.bunq.com/api/1/call/attachment-public-content
4
+ class AttachmentPublicContent
5
+ def initialize(client, id)
6
+ @resource = Bunq::Resource.new(client, "/v1/attachment-public/#{id}/content")
7
+ end
8
+
9
+ ##
10
+ # https://doc.bunq.com/api/1/call/attachment-public-content/method/list
11
+ # Returns the raw content of a public attachment with given ID.
12
+ # The raw content is the binary representation of a file.
13
+ def show
14
+ @resource.with_session do
15
+ @resource.get { |response| response.body }
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,15 @@
1
+ module Bunq
2
+ class Card
3
+ def initialize(parent_resource, id)
4
+ @resource = parent_resource.append("/card/#{id}")
5
+ end
6
+
7
+ def show
8
+ @resource.with_session { @resource.get }['Response']
9
+ end
10
+
11
+ def update(card)
12
+ @resource.with_session { @resource.put(card, encrypt: true) }['Response']
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,11 @@
1
+ module Bunq
2
+ class Cards
3
+ def initialize(parent_resource)
4
+ @resource = parent_resource.append("/card")
5
+ end
6
+
7
+ def index
8
+ @resource.with_session { @resource.get }['Response']
9
+ end
10
+ end
11
+ end
@@ -1,5 +1,6 @@
1
1
  require 'openssl'
2
2
  require 'base64'
3
+ require 'thread_safe'
3
4
 
4
5
  require_relative './version'
5
6
  require_relative './resource'
@@ -7,20 +8,24 @@ require_relative './resource'
7
8
  require_relative './installations'
8
9
  require_relative './installation'
9
10
  require_relative './device_servers'
11
+ require_relative './encryptor'
10
12
  require_relative './session_servers'
11
13
  require_relative './user'
12
14
  require_relative './user_company'
15
+ require_relative './user_person'
13
16
  require_relative './monetary_account'
14
17
  require_relative './monetary_accounts'
18
+ require_relative './payment'
15
19
  require_relative './payments'
16
20
  require_relative './signature'
21
+ require_relative './attachment_public_content.rb'
17
22
 
18
23
  ##
19
24
  # Usage
20
25
  #
21
26
  # Bunq.configure do |config|
22
27
  # config.api_key = 'YOUR_APIKEY'
23
- # config.installation_token = 'YOUR_INSTALLATION_TOKEN'
28
+ # config.installation_token = 'YOUR_INSTALLATION_TOKEN'
24
29
  # config.private_key = 'YOUR PRIVATE KEY'
25
30
  # config.server_public_key = 'SERVER PUBLIC KEY'
26
31
  # end
@@ -38,8 +43,10 @@ module Bunq
38
43
  yield(configuration)
39
44
 
40
45
  configuration.base_url = Configuration::SANDBOX_BASE_URL if configuration.sandbox
46
+ end
41
47
 
42
- fail 'api_key is mandatory' unless self.configuration.api_key
48
+ def reset_configuration
49
+ self.configuration = nil
43
50
  end
44
51
 
45
52
  ##
@@ -49,13 +56,43 @@ module Bunq
49
56
  fail "No configuration! Call Bunq.configure first." unless configuration
50
57
  Client.new(configuration)
51
58
  end
59
+ end
52
60
 
53
- ##
54
- # Returns a new instance of +Signature+
55
- #
56
- def signature
57
- fail "No configuration! Call Bunq.configure first." unless configuration
58
- Signature.new(configuration.private_key, configuration.server_public_key)
61
+ class NoSessionCache
62
+ def get(&block)
63
+ block.call if block_given?
64
+ end
65
+
66
+ def clear
67
+ # no-op
68
+ end
69
+ end
70
+
71
+ ##
72
+ # A thread-safe session cache that can hold one (the current) session.
73
+ #
74
+ # Usage:
75
+ #
76
+ # Bunq.configure do |config|
77
+ # config.session_cache = Bunq::ThreadSafeSessionCache.new
78
+ # end
79
+ #
80
+ # After this, all +Bunq.client+ calls will use the same session. When the session times out,
81
+ # a new one is started automatically.
82
+ #
83
+ class ThreadSafeSessionCache
84
+ CACHE_KEY = 'CURRENT_BUNQ_SESSION'
85
+
86
+ def initialize
87
+ clear
88
+ end
89
+
90
+ def get(&block)
91
+ @cache.fetch_or_store(CACHE_KEY) { block.call if block_given? }
92
+ end
93
+
94
+ def clear
95
+ @cache = ThreadSafe::Cache.new
59
96
  end
60
97
  end
61
98
 
@@ -63,13 +100,15 @@ module Bunq
63
100
  # Configuration object for connecting to the bunq api
64
101
  #
65
102
  class Configuration
66
- SANDBOX_BASE_URL = 'https://sandbox.public.api.bunq.com'
103
+ SANDBOX_BASE_URL = 'https://public-api.sandbox.bunq.com'
67
104
  PRODUCTION_BASE_URL = 'https://api.bunq.com'
68
105
 
69
106
  DEFAULT_LANGUAGE = 'nl_NL'
70
107
  DEFAULT_REGION = 'nl_NL'
71
108
  DEFAULT_GEOLOCATION = '0 0 0 0 000'
72
109
  DEFAULT_USER_AGENT = "bunq ruby client #{Bunq::VERSION}"
110
+ DEFAULT_TIMEOUT = 60
111
+ DEFAULT_SESSION_CACHE = NoSessionCache.new
73
112
 
74
113
  # Base url for the bunq api. Defaults to +PRODUCTION_BASE_URL+
75
114
  attr_accessor :base_url,
@@ -100,8 +139,13 @@ module Bunq
100
139
  # The private key for signing the request
101
140
  :private_key,
102
141
  # The public key of this installation for verifying the response
103
- :server_public_key
104
-
142
+ :server_public_key,
143
+ # Timeout in seconds to wait for bunq api. Defaults to +DEFAULT_TIMEOUT+
144
+ :timeout,
145
+ # Cache to retrieve current session from. Defaults to +DEFAULT_SESSION_CACHE+,
146
+ # which will create a new session per `Bunq.client` instance.
147
+ # See +ThreadSafeSessionCache+ for more advanced use.
148
+ :session_cache
105
149
 
106
150
  def initialize
107
151
  @sandbox = false
@@ -111,6 +155,8 @@ module Bunq
111
155
  @geolocation = DEFAULT_GEOLOCATION
112
156
  @user_agent = DEFAULT_USER_AGENT
113
157
  @disable_response_signature_verification = false
158
+ @timeout = DEFAULT_TIMEOUT
159
+ @session_cache = DEFAULT_SESSION_CACHE
114
160
  end
115
161
  end
116
162
 
@@ -119,11 +165,11 @@ module Bunq
119
165
  #
120
166
  # An instance of a +Client+ can be obtained via +Bunq.client+
121
167
  class Client
122
-
123
168
  attr_accessor :current_session
124
169
  attr_reader :configuration
125
170
 
126
171
  def initialize(configuration)
172
+ fail ArgumentError.new('configuration is required') unless configuration
127
173
  @configuration = configuration
128
174
  end
129
175
 
@@ -151,6 +197,20 @@ module Bunq
151
197
  Bunq::UserCompany.new(self, id)
152
198
  end
153
199
 
200
+ def user_person(id)
201
+ Bunq::UserPerson.new(self, id)
202
+ end
203
+
204
+ # Returns the +Bunq::AttachmentPublicContent+ represented by the given id
205
+ def attachment_public_content(id)
206
+ with_session { Bunq::AttachmentPublicContent.new(self, id) }
207
+ end
208
+
209
+ # Returns the +Bunq::UserPerson+ represented by the +Bunq::Configuration.api_key+
210
+ def me_as_user_person
211
+ with_session { user_person(current_session_user_id) }
212
+ end
213
+
154
214
  # Returns the +Bunq::UserCompany+ represented by the +Bunq::Configuration.api_key+
155
215
  def me_as_user_company
156
216
  with_session { user_company(current_session_user_id) }
@@ -162,12 +222,30 @@ module Bunq
162
222
  end
163
223
 
164
224
  def ensure_session!
165
- @current_session ||= session_servers.create
225
+ @current_session ||= configuration.session_cache.get { create_session }
226
+ end
227
+
228
+ def create_session
229
+ session_servers.create
166
230
  end
167
231
 
168
232
  def with_session(&block)
233
+ retries ||= 0
169
234
  ensure_session!
170
235
  block.call
236
+ rescue UnauthorisedResponse => e
237
+ configuration.session_cache.clear
238
+ @current_session = nil
239
+ retry if (retries += 1) < 2
240
+ raise e
241
+ end
242
+
243
+ def signature
244
+ @signature ||= Signature.new(configuration.private_key, configuration.server_public_key)
245
+ end
246
+
247
+ def encryptor
248
+ @encryptor ||= Encryptor.new(configuration.server_public_key)
171
249
  end
172
250
 
173
251
  def headers
@@ -13,10 +13,20 @@ module Bunq
13
13
 
14
14
  ##
15
15
  # https://doc.bunq.com/api/1/call/device-server/method/post
16
- def create(description)
16
+ #
17
+ # You can add a wildcard IP by passing an array of the current IP,
18
+ # and the `*` character. E.g.: ['1.2.3.4', '*'].
19
+ #
20
+ # @param description [String] The description of this device server.
21
+ # @param permitted_ips [Array|nil] Array of permitted IP addresses.
22
+ def create(description, permitted_ips: nil)
17
23
  fail ArgumentError.new('description is required') unless description
24
+ fail 'Cannot create session, please add the api_key to your configuration' unless @client.configuration.api_key
25
+
26
+ params = { description: description, secret: @client.configuration.api_key }
27
+ params[:permitted_ips] = permitted_ips if permitted_ips
18
28
 
19
- @resource.post(description: description, secret: @client.configuration.api_key)['Response']
29
+ @resource.post(params)['Response']
20
30
  end
21
31
 
22
32
  ##
@@ -0,0 +1,53 @@
1
+ module Bunq
2
+ class Encryptor
3
+ HEADER_CLIENT_ENCRYPTION_HMAC = 'X-Bunq-Client-Encryption-Hmac'
4
+ HEADER_CLIENT_ENCRYPTION_IV = 'X-Bunq-Client-Encryption-Iv'
5
+ HEADER_CLIENT_ENCRYPTION_KEY = 'X-Bunq-Client-Encryption-Key'
6
+ AES_ENCRYPTION_METHOD = 'aes-256-cbc'
7
+ HMAC_ALGORITHM = 'sha1'
8
+
9
+ def initialize(server_public_key)
10
+ fail ArgumentError.new('server_public_key is mandatory') unless server_public_key
11
+
12
+ @server_public_key = OpenSSL::PKey::RSA.new(server_public_key)
13
+ end
14
+
15
+ def encrypt(body)
16
+ headers = {}
17
+
18
+ iv, key, encrypted_body = encrypt_body(body)
19
+
20
+ headers[HEADER_CLIENT_ENCRYPTION_IV] = Base64.strict_encode64(iv)
21
+
22
+ encrypted_key = server_public_key.public_encrypt(key)
23
+ headers[HEADER_CLIENT_ENCRYPTION_KEY] = Base64.strict_encode64(encrypted_key)
24
+
25
+ digest = hmac(key, iv + encrypted_body)
26
+ headers[HEADER_CLIENT_ENCRYPTION_HMAC] = Base64.strict_encode64(digest)
27
+
28
+ [encrypted_body, headers]
29
+ end
30
+
31
+ private
32
+
33
+ attr_reader :server_public_key
34
+
35
+ def encrypt_body(body)
36
+ cipher = OpenSSL::Cipher.new(AES_ENCRYPTION_METHOD)
37
+ cipher.encrypt
38
+
39
+ iv = cipher.random_iv
40
+ key = cipher.random_key
41
+
42
+ encrypted_body = cipher.update(body) + cipher.final
43
+
44
+ [iv, key, encrypted_body]
45
+ end
46
+
47
+ def hmac(key, content)
48
+ hmac = OpenSSL::HMAC.new(key, OpenSSL::Digest.new(HMAC_ALGORITHM))
49
+ hmac << content
50
+ hmac.digest
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,33 @@
1
+ module Bunq
2
+ class ResponseError < StandardError
3
+ attr_reader :code
4
+ attr_reader :headers
5
+ attr_reader :body
6
+
7
+ def initialize(msg = "Response error", code: nil, headers: nil, body: nil)
8
+ @code = code
9
+ @headers = headers || {}
10
+ @body = body
11
+ super("#{msg}: #{body}")
12
+ end
13
+
14
+ # Returns the parsed body if it is a JSON document, nil otherwise.
15
+ # @param opts [Hash] Optional options that are passed to `JSON.parse`.
16
+ def parsed_body(opts = {})
17
+ JSON.parse(@body, opts) if @body && @headers['content-type'] && @headers['content-type'].include?('application/json')
18
+ end
19
+
20
+ # Returns an array of errors returned from the API, or nil if no errors are returned.
21
+ # @return [Array|nil]
22
+ def errors
23
+ json = parsed_body
24
+ json ? json['Error'] : nil
25
+ end
26
+ end
27
+
28
+ class UnexpectedResponse < ResponseError; end
29
+ class AbsentResponseSignature < ResponseError; end
30
+ class TooManyRequestsResponse < ResponseError; end
31
+ class UnauthorisedResponse < ResponseError; end
32
+ class Timeout < StandardError; end
33
+ end
@@ -7,7 +7,7 @@ module Bunq
7
7
  def create(public_key)
8
8
  fail ArgumentError.new('public_key is required') unless public_key
9
9
 
10
- @resource.post({client_public_key: public_key}, true)['Response']
10
+ @resource.post({client_public_key: public_key}, skip_verify: true)['Response']
11
11
  end
12
12
 
13
13
  def index
@@ -6,6 +6,10 @@ module Bunq
6
6
  @resource = parent_resource.append("/monetary-account/#{id}")
7
7
  end
8
8
 
9
+ def payment(id)
10
+ Bunq::Payment.new(@resource, id)
11
+ end
12
+
9
13
  def payments
10
14
  Bunq::Payments.new(@resource)
11
15
  end
@@ -1,4 +1,4 @@
1
- require_relative 'unexpected_response'
1
+ require_relative 'errors'
2
2
 
3
3
  module Bunq
4
4
  class MissingPaginationObject < UnexpectedResponse; end
@@ -40,12 +40,22 @@ module Bunq
40
40
  pagination = result['Pagination']
41
41
  fail MissingPaginationObject unless pagination
42
42
 
43
- last_page = !pagination['older_url']
44
- next_params = params.merge(older_id: param('older_id', pagination['older_url'])) unless last_page
43
+ last_page = !pagination[paging_url(params)]
44
+ next_params = params.merge(:"#{paging_id(params)}" => param(paging_id(params), pagination[paging_url(params)])) unless last_page
45
45
  end
46
46
  end
47
47
  end
48
48
 
49
+ def paging_url(params)
50
+ return 'newer_url' if params[:newer_id]
51
+ 'older_url'
52
+ end
53
+
54
+ def paging_id(params)
55
+ return 'newer_id' if params[:newer_id]
56
+ 'older_id'
57
+ end
58
+
49
59
  def param(name, url)
50
60
  CGI.parse(URI(url).query)[name]&.first
51
61
  end
@@ -0,0 +1,13 @@
1
+ module Bunq
2
+ # https://doc.bunq.com/api/1/call/payment
3
+ class Payment
4
+ def initialize(parent_resource, id)
5
+ @resource = parent_resource.append("/payment/#{id}")
6
+ end
7
+
8
+ # https://doc.bunq.com/api/1/call/payment/method/get
9
+ def show
10
+ @resource.with_session { @resource.get }['Response']
11
+ end
12
+ end
13
+ end
@@ -1,5 +1,4 @@
1
- require_relative 'signature'
2
- require_relative 'unexpected_response'
1
+ require_relative 'errors'
3
2
  require 'restclient'
4
3
  require 'json'
5
4
 
@@ -11,74 +10,89 @@ module Bunq
11
10
  def initialize(client, path)
12
11
  @client = client
13
12
  @path = path
14
- @resource = RestClient::Resource.new(
15
- "#{client.configuration.base_url}#{path}",
16
- {
17
- headers: client.headers
18
- }.tap do |x|
19
- if client.configuration.sandbox
20
- x[:user] = client.configuration.sandbox_user
21
- x[:password] = client.configuration.sandbox_password
22
- end
23
- end
24
- )
25
13
  end
26
14
 
27
15
  def get(params = {}, &block)
28
- @resource.get({params: params}.merge(bunq_request_headers('GET', params))) do |response, request, result|
16
+ resource.get({params: params}.merge(bunq_request_headers('GET', params))) do |response, request, result|
29
17
  verify_and_handle_response(response, request, result, &block)
30
18
  end
19
+ rescue RestClient::Exceptions::Timeout
20
+ raise Bunq::Timeout
31
21
  end
32
22
 
33
23
 
34
- def post(payload, skip_verify = false, &block)
35
- json = JSON.generate(payload)
36
- if skip_verify
37
- @resource.post(json, bunq_request_headers('POST', NO_PARAMS, json)) do |response, request, result|
24
+ def post(payload, skip_verify: false, encrypt: false, &block)
25
+ body = JSON.generate(payload)
26
+ body, headers = client.encryptor.encrypt(body) if encrypt
27
+
28
+ headers = bunq_request_headers('POST', NO_PARAMS, body, headers || {})
29
+
30
+ resource.post(body, headers) do |response, request, result|
31
+ if skip_verify
38
32
  handle_response(response, request, result, &block)
39
- end
40
- else
41
- @resource.post(json, bunq_request_headers('POST', NO_PARAMS, json)) do |response, request, result|
33
+ else
42
34
  verify_and_handle_response(response, request, result, &block)
43
35
  end
44
36
  end
37
+ rescue RestClient::Exceptions::Timeout
38
+ raise Bunq::Timeout
45
39
  end
46
40
 
47
- def put(payload, &block)
48
- json = JSON.generate(payload)
49
- @resource.put(json, bunq_request_headers('PUT', NO_PARAMS, json)) do |response, request, result|
41
+ def put(payload, encrypt: false, &block)
42
+ body = JSON.generate(payload)
43
+ body, headers = client.encryptor.encrypt(body) if encrypt
44
+
45
+ headers = bunq_request_headers('PUT', NO_PARAMS, body, headers || {})
46
+
47
+ resource.put(body, headers) do |response, request, result|
50
48
  verify_and_handle_response(response, request, result, &block)
51
49
  end
50
+ rescue RestClient::Exceptions::Timeout
51
+ raise Bunq::Timeout
52
52
  end
53
53
 
54
54
  def append(path)
55
55
  Bunq::Resource.new(client, @path + path)
56
56
  end
57
57
 
58
- def ensure_session!
59
- client.ensure_session!
60
- end
61
-
62
58
  def with_session(&block)
63
59
  client.with_session(&block)
64
60
  end
65
61
 
66
62
  private
67
63
 
68
- attr_reader :client
64
+ attr_reader :client, :path
69
65
 
70
- def bunq_request_headers(verb, params, payload = nil)
71
- request_id_header = {'X-Bunq-Client-Request-Id' => SecureRandom.uuid}
66
+ def resource
67
+ RestClient::Resource.new(
68
+ "#{client.configuration.base_url}#{path}",
69
+ {
70
+ headers: client.headers,
71
+ timeout: client.configuration.timeout,
72
+ }.tap do |x|
73
+ if client.configuration.sandbox
74
+ x[:user] = client.configuration.sandbox_user
75
+ x[:password] = client.configuration.sandbox_password
76
+ end
77
+ end
78
+ )
79
+ end
80
+
81
+ def bunq_request_headers(verb, params, payload = nil, headers = {})
82
+ headers['X-Bunq-Client-Request-Id'] = SecureRandom.uuid
83
+
84
+ unless @path.end_with?('/installation') && verb == 'POST'
85
+ headers['X-Bunq-Client-Signature'] = sign_request(verb, params, headers, payload)
86
+ end
72
87
 
73
- return request_id_header if @path.end_with?('/installation') && verb == 'POST'
74
- request_id_header.merge('X-Bunq-Client-Signature' => sign_request(verb, params, request_id_header, payload))
88
+ headers
75
89
  end
76
90
 
77
- def sign_request(verb, params, request_id_header, payload = nil)
78
- Bunq.signature.create(
91
+ def sign_request(verb, params, headers, payload = nil)
92
+ client.signature.create(
79
93
  verb,
80
94
  encode_params(@path, params),
81
- @resource.headers.merge(request_id_header),
95
+ resource.headers.merge(headers),
82
96
  payload
83
97
  )
84
98
  end
@@ -89,18 +103,21 @@ module Bunq
89
103
  end
90
104
 
91
105
  def verify_and_handle_response(response, request, result, &block)
92
- Bunq.signature.verify!(response) unless client.configuration.disable_response_signature_verification
106
+ client.signature.verify!(response) unless client.configuration.disable_response_signature_verification
93
107
  handle_response(response, request, result, &block)
94
108
  end
95
109
 
96
110
  def handle_response(response, _request, _result, &block)
97
- case response.code
98
- when 200, 201
111
+ if response.code == 200 || response.code == 201
99
112
  if block_given?
100
113
  yield(response)
101
114
  else
102
115
  JSON.parse(response.body)
103
116
  end
117
+ elsif (response.code == 409 && Bunq.configuration.sandbox) || response.code == 429
118
+ fail TooManyRequestsResponse.new(code: response.code, headers: response.raw_headers, body: response.body)
119
+ elsif response.code == 401
120
+ fail UnauthorisedResponse.new(code: response.code, headers: response.raw_headers, body: response.body)
104
121
  else
105
122
  fail UnexpectedResponse.new(code: response.code, headers: response.raw_headers, body: response.body)
106
123
  end
@@ -10,7 +10,7 @@ module Bunq
10
10
  ##
11
11
  # https://doc.bunq.com/api/1/call/session-server/method/post
12
12
  def create
13
- fail 'Cannot create session, please provide api_key to Bunq::Client' unless @api_key
13
+ fail 'Cannot create session, please add the api_key to your configuration' unless @api_key
14
14
  @resource.post(secret: @api_key)['Response']
15
15
  end
16
16
  end
@@ -1,4 +1,4 @@
1
- require_relative 'unexpected_response'
1
+ require_relative 'errors'
2
2
 
3
3
  module Bunq
4
4
  class Signature
@@ -27,13 +27,18 @@ module Bunq
27
27
  end
28
28
 
29
29
  def verify!(response)
30
+ return if skip_signature_check(response.code)
31
+
30
32
  sorted_bunq_headers = response.raw_headers.select(&method(:verifiable_header?)).sort.to_h.map { |k, v| "#{k.to_s.split('-').map(&:capitalize).join('-')}: #{v.first}" }
31
33
  data = %Q{#{response.code}\n#{sorted_bunq_headers.join("\n")}\n\n#{response.body}}
32
34
 
33
- signature_headers = response.raw_headers.find { |k, _| k.to_s.downcase == BUNQ_SERVER_SIGNATURE_RESPONSE_HEADER }[1]
34
- fail UnexpectedResponse.new(code: response.code, headers: response.raw_headers, body: response.body) unless signature_headers
35
+ signature_headers = response.raw_headers.find { |k, _| k.to_s.downcase == BUNQ_SERVER_SIGNATURE_RESPONSE_HEADER }
36
+ fail AbsentResponseSignature.new(code: response.code, headers: response.raw_headers, body: response.body) unless signature_headers
37
+
38
+ signature_headers_value = signature_headers[1]
39
+ fail AbsentResponseSignature.new(code: response.code, headers: response.raw_headers, body: response.body) unless signature_headers_value
35
40
 
36
- signature = Base64.strict_decode64(signature_headers.first)
41
+ signature = Base64.strict_decode64(signature_headers_value.first)
37
42
  fail UnexpectedResponse.new(code: response.code, headers: response.raw_headers, body: response.body) unless server_public_key.verify(digest, signature, data)
38
43
  end
39
44
 
@@ -63,5 +68,9 @@ module Bunq
63
68
  _header_name = header_name.to_s.downcase
64
69
  _header_name.start_with?(BUNQ_HEADER_PREFIX) && _header_name != BUNQ_SERVER_SIGNATURE_RESPONSE_HEADER
65
70
  end
71
+
72
+ def skip_signature_check(responseCode)
73
+ (Bunq::configuration.sandbox && responseCode == 409) || responseCode == 429
74
+ end
66
75
  end
67
76
  end
@@ -4,6 +4,8 @@ require_relative 'monetary_accounts'
4
4
  require_relative 'draft_share_invite_bank'
5
5
  require_relative 'draft_share_invite_banks'
6
6
  require_relative 'certificate_pinned'
7
+ require_relative 'card'
8
+ require_relative 'cards'
7
9
 
8
10
  module Bunq
9
11
  class User
@@ -31,6 +33,14 @@ module Bunq
31
33
  Bunq::CertificatePinned.new(@resource)
32
34
  end
33
35
 
36
+ def card(id)
37
+ Bunq::Card.new(@resource, id)
38
+ end
39
+
40
+ def cards
41
+ Bunq::Cards.new(@resource)
42
+ end
43
+
34
44
  def show
35
45
  @resource.with_session { @resource.get }['Response']
36
46
  end
@@ -0,0 +1,17 @@
1
+ require_relative 'resource'
2
+
3
+ module Bunq
4
+ class UserPerson
5
+ def initialize(client, id)
6
+ @resource = Bunq::Resource.new(client, "/v1/user-person/#{id}")
7
+ end
8
+
9
+ def show
10
+ @resource.with_session { @resource.get }['Response']
11
+ end
12
+
13
+ def update(attributes)
14
+ @resource.with_session { @resource.put(attributes) }['Response']
15
+ end
16
+ end
17
+ end
@@ -1,3 +1,3 @@
1
1
  module Bunq
2
- VERSION = "0.1.2"
2
+ VERSION = "0.2.0"
3
3
  end
metadata CHANGED
@@ -1,16 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bunq-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lars Vonk
8
8
  - Bob Forma
9
9
  - Derek Kraan
10
+ - Mike van Diepen
10
11
  autorequire:
11
12
  bindir: exe
12
13
  cert_chain: []
13
- date: 2017-05-19 00:00:00.000000000 Z
14
+ date: 2019-06-03 00:00:00.000000000 Z
14
15
  dependencies:
15
16
  - !ruby/object:Gem::Dependency
16
17
  name: rest-client
@@ -26,62 +27,76 @@ dependencies:
26
27
  - - "~>"
27
28
  - !ruby/object:Gem::Version
28
29
  version: '2.0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: thread_safe
32
+ requirement: !ruby/object:Gem::Requirement
33
+ requirements:
34
+ - - "~>"
35
+ - !ruby/object:Gem::Version
36
+ version: 0.3.6
37
+ type: :runtime
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - "~>"
42
+ - !ruby/object:Gem::Version
43
+ version: 0.3.6
29
44
  - !ruby/object:Gem::Dependency
30
45
  name: bundler
31
46
  requirement: !ruby/object:Gem::Requirement
32
47
  requirements:
33
48
  - - "~>"
34
49
  - !ruby/object:Gem::Version
35
- version: '1.13'
50
+ version: '1.16'
36
51
  type: :development
37
52
  prerelease: false
38
53
  version_requirements: !ruby/object:Gem::Requirement
39
54
  requirements:
40
55
  - - "~>"
41
56
  - !ruby/object:Gem::Version
42
- version: '1.13'
57
+ version: '1.16'
43
58
  - !ruby/object:Gem::Dependency
44
59
  name: rake
45
60
  requirement: !ruby/object:Gem::Requirement
46
61
  requirements:
47
62
  - - "~>"
48
63
  - !ruby/object:Gem::Version
49
- version: '10.0'
64
+ version: '12.3'
50
65
  type: :development
51
66
  prerelease: false
52
67
  version_requirements: !ruby/object:Gem::Requirement
53
68
  requirements:
54
69
  - - "~>"
55
70
  - !ruby/object:Gem::Version
56
- version: '10.0'
71
+ version: '12.3'
57
72
  - !ruby/object:Gem::Dependency
58
73
  name: rspec
59
74
  requirement: !ruby/object:Gem::Requirement
60
75
  requirements:
61
76
  - - "~>"
62
77
  - !ruby/object:Gem::Version
63
- version: '3.5'
78
+ version: '3.7'
64
79
  type: :development
65
80
  prerelease: false
66
81
  version_requirements: !ruby/object:Gem::Requirement
67
82
  requirements:
68
83
  - - "~>"
69
84
  - !ruby/object:Gem::Version
70
- version: '3.5'
85
+ version: '3.7'
71
86
  - !ruby/object:Gem::Dependency
72
87
  name: webmock
73
88
  requirement: !ruby/object:Gem::Requirement
74
89
  requirements:
75
90
  - - "~>"
76
91
  - !ruby/object:Gem::Version
77
- version: 2.3.2
92
+ version: 3.2.1
78
93
  type: :development
79
94
  prerelease: false
80
95
  version_requirements: !ruby/object:Gem::Requirement
81
96
  requirements:
82
97
  - - "~>"
83
98
  - !ruby/object:Gem::Version
84
- version: 2.3.2
99
+ version: 3.2.1
85
100
  - !ruby/object:Gem::Dependency
86
101
  name: rspec-json_expectations
87
102
  requirement: !ruby/object:Gem::Requirement
@@ -114,7 +129,8 @@ description: Ruby client for the bunq public API. Extracted from www.jortt.nl
114
129
  email:
115
130
  - lars.vonk@gmail.com
116
131
  - bforma@zilverline.com
117
- - dkraan@zilverline.com
132
+ - derek.kraan@gmail.com
133
+ - mvdiepen@zilverline.com
118
134
  executables: []
119
135
  extensions: []
120
136
  extra_rdoc_files: []
@@ -129,25 +145,31 @@ files:
129
145
  - README.md
130
146
  - Rakefile
131
147
  - bunq-client.gemspec
148
+ - lib/bunq/attachment_public_content.rb
132
149
  - lib/bunq/bunq.rb
150
+ - lib/bunq/card.rb
151
+ - lib/bunq/cards.rb
133
152
  - lib/bunq/certificate_pinned.rb
134
153
  - lib/bunq/client.rb
135
154
  - lib/bunq/device_servers.rb
136
155
  - lib/bunq/draft_share_invite_bank.rb
137
156
  - lib/bunq/draft_share_invite_banks.rb
157
+ - lib/bunq/encryptor.rb
158
+ - lib/bunq/errors.rb
138
159
  - lib/bunq/installation.rb
139
160
  - lib/bunq/installations.rb
140
161
  - lib/bunq/monetary_account.rb
141
162
  - lib/bunq/monetary_accounts.rb
142
163
  - lib/bunq/paginated.rb
164
+ - lib/bunq/payment.rb
143
165
  - lib/bunq/payments.rb
144
166
  - lib/bunq/qr_code_content.rb
145
167
  - lib/bunq/resource.rb
146
168
  - lib/bunq/session_servers.rb
147
169
  - lib/bunq/signature.rb
148
- - lib/bunq/unexpected_response.rb
149
170
  - lib/bunq/user.rb
150
171
  - lib/bunq/user_company.rb
172
+ - lib/bunq/user_person.rb
151
173
  - lib/bunq/version.rb
152
174
  homepage: https://github.com/jorttbv/bunq-client
153
175
  licenses:
@@ -169,7 +191,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
169
191
  version: '0'
170
192
  requirements: []
171
193
  rubyforge_project:
172
- rubygems_version: 2.5.2
194
+ rubygems_version: 2.7.6
173
195
  signing_key:
174
196
  specification_version: 4
175
197
  summary: Ruby client for the bunq public API.
@@ -1,5 +0,0 @@
1
- module Bunq
2
-
3
- class UnexpectedResponse < StandardError; end
4
-
5
- end