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 +5 -5
- data/.ruby-version +2 -1
- data/.travis.yml +2 -2
- data/README.md +11 -0
- data/bunq-client.gemspec +8 -6
- data/lib/bunq/attachment_public_content.rb +19 -0
- data/lib/bunq/card.rb +15 -0
- data/lib/bunq/cards.rb +11 -0
- data/lib/bunq/client.rb +91 -13
- data/lib/bunq/device_servers.rb +12 -2
- data/lib/bunq/encryptor.rb +53 -0
- data/lib/bunq/errors.rb +33 -0
- data/lib/bunq/installations.rb +1 -1
- data/lib/bunq/monetary_account.rb +4 -0
- data/lib/bunq/paginated.rb +13 -3
- data/lib/bunq/payment.rb +13 -0
- data/lib/bunq/resource.rb +56 -39
- data/lib/bunq/session_servers.rb +1 -1
- data/lib/bunq/signature.rb +13 -4
- data/lib/bunq/user.rb +10 -0
- data/lib/bunq/user_person.rb +17 -0
- data/lib/bunq/version.rb +1 -1
- metadata +35 -13
- data/lib/bunq/unexpected_response.rb +0 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f43869cbdc6e1d596f972aac0752ca7ed25fe6884814ea592a7e34e97fae6948
|
4
|
+
data.tar.gz: 38a49c91d8e233921b8d21b1e5852e01acfff630157abe0dd3bfa4f715f4a10f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a853e3ae7694c1bfb5a41aa735c4942ff015c8614602f6efd45d9a9aa08300ee1e89da4ad2ff3f354d82ef4ba519f10229badd1338dd9730d61ec8d8b2cb475d
|
7
|
+
data.tar.gz: ac8ee43d4bb61f30ce45937b7b90af82087cccd0d248396d27decdcbcdfae6584f4bf40f5c434135cc9ac7d248ebafdd9f44039557d59a433b8124ab1a06e4d2
|
data/.ruby-version
CHANGED
@@ -1 +1,2 @@
|
|
1
|
-
2.3
|
1
|
+
2.5.3
|
2
|
+
|
data/.travis.yml
CHANGED
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.
|
data/bunq-client.gemspec
CHANGED
@@ -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
|
-
'
|
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.
|
36
|
-
spec.add_development_dependency "rake", "~>
|
37
|
-
spec.add_development_dependency "rspec", "~> 3.
|
38
|
-
spec.add_development_dependency "webmock", "~>
|
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
|
data/lib/bunq/card.rb
ADDED
@@ -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
|
data/lib/bunq/cards.rb
ADDED
data/lib/bunq/client.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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://
|
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 ||=
|
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
|
data/lib/bunq/device_servers.rb
CHANGED
@@ -13,10 +13,20 @@ module Bunq
|
|
13
13
|
|
14
14
|
##
|
15
15
|
# https://doc.bunq.com/api/1/call/device-server/method/post
|
16
|
-
|
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(
|
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
|
data/lib/bunq/errors.rb
ADDED
@@ -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
|
data/lib/bunq/installations.rb
CHANGED
@@ -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
|
data/lib/bunq/paginated.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
require_relative '
|
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[
|
44
|
-
next_params = params.merge(
|
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
|
data/lib/bunq/payment.rb
ADDED
@@ -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
|
data/lib/bunq/resource.rb
CHANGED
@@ -1,5 +1,4 @@
|
|
1
|
-
require_relative '
|
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
|
-
|
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
|
35
|
-
|
36
|
-
if
|
37
|
-
|
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
|
-
|
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
|
-
|
49
|
-
|
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
|
71
|
-
|
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
|
-
|
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,
|
78
|
-
|
91
|
+
def sign_request(verb, params, headers, payload = nil)
|
92
|
+
client.signature.create(
|
79
93
|
verb,
|
80
94
|
encode_params(@path, params),
|
81
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/bunq/session_servers.rb
CHANGED
@@ -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
|
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
|
data/lib/bunq/signature.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
require_relative '
|
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 }
|
34
|
-
fail
|
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(
|
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
|
data/lib/bunq/user.rb
CHANGED
@@ -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
|
data/lib/bunq/version.rb
CHANGED
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.
|
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:
|
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.
|
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.
|
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: '
|
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: '
|
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.
|
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.
|
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:
|
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:
|
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
|
-
-
|
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.
|
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.
|