bunq-client 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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