bunq-client 0.6.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/rspec.yml +40 -0
- data/.rubocop.yml +212 -0
- data/.ruby-version +1 -1
- data/CHANGELOG.md +4 -0
- data/Gemfile +2 -0
- data/README.md +6 -4
- data/Rakefile +7 -3
- data/bunq-client.gemspec +22 -20
- data/lib/bunq/attachment.rb +15 -0
- data/lib/bunq/attachment_public.rb +13 -0
- data/lib/bunq/attachment_public_content.rb +4 -2
- data/lib/bunq/attachment_publics.rb +20 -0
- data/lib/bunq/attachments.rb +20 -0
- data/lib/bunq/avatar.rb +15 -0
- data/lib/bunq/avatars.rb +15 -0
- data/lib/bunq/bunq.rb +2 -0
- data/lib/bunq/bunqme_tab.rb +17 -0
- data/lib/bunq/bunqme_tabs.rb +21 -0
- data/lib/bunq/card.rb +2 -0
- data/lib/bunq/cards.rb +3 -1
- data/lib/bunq/certificate_pinned.rb +10 -6
- data/lib/bunq/client.rb +74 -51
- data/lib/bunq/device_servers.rb +5 -4
- data/lib/bunq/draft_share_invite_bank.rb +2 -0
- data/lib/bunq/draft_share_invite_banks.rb +3 -1
- data/lib/bunq/encryptor.rb +6 -7
- data/lib/bunq/errors.rb +10 -3
- data/lib/bunq/header.rb +20 -0
- data/lib/bunq/installation.rb +2 -0
- data/lib/bunq/installations.rb +4 -2
- data/lib/bunq/monetary_account.rb +17 -0
- data/lib/bunq/monetary_account_bank.rb +17 -0
- data/lib/bunq/monetary_account_banks.rb +21 -0
- data/lib/bunq/monetary_accounts.rb +3 -1
- data/lib/bunq/notification_filter_url.rb +4 -2
- data/lib/bunq/paginated.rb +14 -3
- data/lib/bunq/payment.rb +2 -0
- data/lib/bunq/payments.rb +3 -1
- data/lib/bunq/qr_code_content.rb +5 -3
- data/lib/bunq/resource.rb +34 -16
- data/lib/bunq/session_servers.rb +5 -2
- data/lib/bunq/signature.rb +31 -8
- data/lib/bunq/user.rb +17 -0
- data/lib/bunq/user_company.rb +2 -0
- data/lib/bunq/user_person.rb +2 -0
- data/lib/bunq/users.rb +17 -0
- data/lib/bunq/version.rb +3 -1
- metadata +53 -20
- data/.travis.yml +0 -5
data/lib/bunq/errors.rb
CHANGED
@@ -1,10 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Bunq
|
2
4
|
class ResponseError < StandardError
|
3
5
|
attr_reader :code
|
4
6
|
attr_reader :headers
|
5
7
|
attr_reader :body
|
6
8
|
|
7
|
-
def initialize(msg =
|
9
|
+
def initialize(msg = 'Response error', code: nil, headers: nil, body: nil)
|
8
10
|
@code = code
|
9
11
|
@headers = headers || {}
|
10
12
|
@body = body
|
@@ -14,7 +16,12 @@ module Bunq
|
|
14
16
|
# Returns the parsed body if it is a JSON document, nil otherwise.
|
15
17
|
# @param opts [Hash] Optional options that are passed to `JSON.parse`.
|
16
18
|
def parsed_body(opts = {})
|
17
|
-
|
19
|
+
if @body && @headers['content-type'] && @headers['content-type'].include?('application/json')
|
20
|
+
JSON.parse(
|
21
|
+
@body,
|
22
|
+
opts,
|
23
|
+
)
|
24
|
+
end
|
18
25
|
end
|
19
26
|
|
20
27
|
# Returns an array of errors returned from the API, or nil if no errors are returned.
|
@@ -26,7 +33,7 @@ module Bunq
|
|
26
33
|
end
|
27
34
|
|
28
35
|
class UnexpectedResponse < ResponseError; end
|
29
|
-
class
|
36
|
+
class InvalidResponseSignature < ResponseError; end
|
30
37
|
class AbsentResponseSignature < ResponseError; end
|
31
38
|
class TooManyRequestsResponse < ResponseError; end
|
32
39
|
class UnauthorisedResponse < ResponseError; end
|
data/lib/bunq/header.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bunq
|
4
|
+
module Header
|
5
|
+
ACCEPT = 'Accept'
|
6
|
+
ATTACHMENT_DESCRIPTION = 'X-Bunq-Attachment-Description'
|
7
|
+
CACHE_CONTROL = 'Cache-Control'
|
8
|
+
CONTENT_TYPE = 'Content-Type'
|
9
|
+
CLIENT_AUTH = 'X-Bunq-Client-Authentication'
|
10
|
+
CLIENT_ENCRYPTION_HMAC = 'X-Bunq-Client-Encryption-Hmac'
|
11
|
+
CLIENT_ENCRYPTION_IV = 'X-Bunq-Client-Encryption-Iv'
|
12
|
+
CLIENT_ENCRYPTION_KEY = 'X-Bunq-Client-Encryption-Key'
|
13
|
+
CLIENT_REQUEST_ID = 'X-Bunq-Client-Request-Id'
|
14
|
+
CLIENT_SIGNATURE = 'X-Bunq-Client-Signature'
|
15
|
+
GEOLOCATION = 'X-Bunq-Geolocation'
|
16
|
+
LANGUAGE = 'X-Bunq-Language'
|
17
|
+
REGION = 'X-Bunq-Region'
|
18
|
+
USER_AGENT = 'User-Agent'
|
19
|
+
end
|
20
|
+
end
|
data/lib/bunq/installation.rb
CHANGED
data/lib/bunq/installations.rb
CHANGED
@@ -1,11 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Bunq
|
2
4
|
class Installations
|
3
5
|
def initialize(client)
|
4
|
-
@resource = Bunq::Resource.new(client,
|
6
|
+
@resource = Bunq::Resource.new(client, '/v1/installation')
|
5
7
|
end
|
6
8
|
|
7
9
|
def create(public_key)
|
8
|
-
fail ArgumentError
|
10
|
+
fail ArgumentError, 'public_key is required' unless public_key
|
9
11
|
|
10
12
|
@resource.post({client_public_key: public_key}, skip_verify: true)['Response']
|
11
13
|
end
|
@@ -1,4 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'attachments'
|
1
4
|
require_relative 'notification_filter_url'
|
5
|
+
require_relative 'bunqme_tab'
|
6
|
+
require_relative 'bunqme_tabs'
|
2
7
|
|
3
8
|
module Bunq
|
4
9
|
##
|
@@ -8,6 +13,18 @@ module Bunq
|
|
8
13
|
@resource = parent_resource.append("/monetary-account/#{id}")
|
9
14
|
end
|
10
15
|
|
16
|
+
def attachments
|
17
|
+
Bunq::Attachments.new(@resource)
|
18
|
+
end
|
19
|
+
|
20
|
+
def bunqme_tab(id)
|
21
|
+
Bunq::BunqmeTab.new(@resource, id)
|
22
|
+
end
|
23
|
+
|
24
|
+
def bunqme_tabs
|
25
|
+
Bunq::BunqmeTabs.new(@resource)
|
26
|
+
end
|
27
|
+
|
11
28
|
def payment(id)
|
12
29
|
Bunq::Payment.new(@resource, id)
|
13
30
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bunq
|
4
|
+
class MonetaryAccountBank
|
5
|
+
def initialize(parent_resource, id)
|
6
|
+
@resource = parent_resource.append("/monetary-account-bank/#{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
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'paginated'
|
4
|
+
|
5
|
+
module Bunq
|
6
|
+
class MonetaryAccountBanks
|
7
|
+
def initialize(parent_resource)
|
8
|
+
@resource = parent_resource.append('/monetary-account-bank')
|
9
|
+
end
|
10
|
+
|
11
|
+
def index(count: 200, older_id: nil, newer_id: nil)
|
12
|
+
Bunq::Paginated
|
13
|
+
.new(@resource)
|
14
|
+
.paginate(count: count, older_id: older_id, newer_id: newer_id)
|
15
|
+
end
|
16
|
+
|
17
|
+
def create(attributes)
|
18
|
+
@resource.with_session { @resource.post(attributes) }['Response']
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -1,9 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Bunq
|
2
4
|
##
|
3
5
|
# https://doc.bunq.com/api/1/call/monetary-account
|
4
6
|
class MonetaryAccounts
|
5
7
|
def initialize(parent_resource)
|
6
|
-
@resource = parent_resource.append(
|
8
|
+
@resource = parent_resource.append('/monetary-account')
|
7
9
|
end
|
8
10
|
|
9
11
|
# https://doc.bunq.com/api/1/call/monetary-account-bank/method/list
|
@@ -1,12 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Bunq
|
2
4
|
# https://doc.bunq.com/#/notification-filter-url
|
3
5
|
class NotificationFilterUrl
|
4
6
|
def initialize(parent_resource)
|
5
|
-
@resource = parent_resource.append(
|
7
|
+
@resource = parent_resource.append('/notification-filter-url')
|
6
8
|
end
|
7
9
|
|
8
10
|
def create(notification_filters)
|
9
|
-
@resource.with_session { @resource.post({notification_filters: notification_filters})}['Response']
|
11
|
+
@resource.with_session { @resource.post({notification_filters: notification_filters}) }['Response']
|
10
12
|
end
|
11
13
|
|
12
14
|
def show
|
data/lib/bunq/paginated.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative 'errors'
|
2
4
|
|
3
5
|
module Bunq
|
@@ -16,7 +18,7 @@ module Bunq
|
|
16
18
|
private
|
17
19
|
|
18
20
|
def setup_params(count, older_id, newer_id)
|
19
|
-
fail ArgumentError
|
21
|
+
fail ArgumentError, 'Cant pass both older_id and newer_id' if older_id && newer_id
|
20
22
|
|
21
23
|
params = {count: count}
|
22
24
|
params[:older_id] = older_id if older_id
|
@@ -30,7 +32,7 @@ module Bunq
|
|
30
32
|
|
31
33
|
Enumerator.new do |yielder|
|
32
34
|
loop do
|
33
|
-
|
35
|
+
fail StopIteration if last_page
|
34
36
|
|
35
37
|
result = @resource.with_session { @resource.get(next_params) }
|
36
38
|
result['Response'].each do |item|
|
@@ -41,18 +43,27 @@ module Bunq
|
|
41
43
|
fail MissingPaginationObject unless pagination
|
42
44
|
|
43
45
|
last_page = !pagination[paging_url(params)]
|
44
|
-
|
46
|
+
next if last_page
|
47
|
+
|
48
|
+
next_params = params.merge(
|
49
|
+
"#{paging_id(params)}": param(
|
50
|
+
paging_id(params),
|
51
|
+
pagination[paging_url(params)],
|
52
|
+
),
|
53
|
+
)
|
45
54
|
end
|
46
55
|
end
|
47
56
|
end
|
48
57
|
|
49
58
|
def paging_url(params)
|
50
59
|
return 'newer_url' if params[:newer_id]
|
60
|
+
|
51
61
|
'older_url'
|
52
62
|
end
|
53
63
|
|
54
64
|
def paging_id(params)
|
55
65
|
return 'newer_id' if params[:newer_id]
|
66
|
+
|
56
67
|
'older_id'
|
57
68
|
end
|
58
69
|
|
data/lib/bunq/payment.rb
CHANGED
data/lib/bunq/payments.rb
CHANGED
@@ -1,10 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative 'paginated'
|
2
4
|
|
3
5
|
module Bunq
|
4
6
|
# https://doc.bunq.com/api/1/call/payment
|
5
7
|
class Payments
|
6
8
|
def initialize(parent_resource)
|
7
|
-
@resource = parent_resource.append(
|
9
|
+
@resource = parent_resource.append('/payment')
|
8
10
|
end
|
9
11
|
|
10
12
|
# https://doc.bunq.com/api/1/call/payment/method/list
|
data/lib/bunq/qr_code_content.rb
CHANGED
@@ -1,12 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Bunq
|
2
4
|
class QrCodeContent
|
3
5
|
def initialize(parent_resource)
|
4
|
-
@resource = parent_resource.append(
|
6
|
+
@resource = parent_resource.append('/qr-code-content')
|
5
7
|
end
|
6
8
|
|
7
9
|
def show
|
8
|
-
@resource.with_session do
|
9
|
-
@resource.get
|
10
|
+
@resource.with_session do
|
11
|
+
@resource.get(&:body)
|
10
12
|
end
|
11
13
|
end
|
12
14
|
end
|
data/lib/bunq/resource.rb
CHANGED
@@ -1,11 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative 'errors'
|
2
4
|
require 'restclient'
|
3
5
|
require 'json'
|
4
6
|
|
5
7
|
module Bunq
|
6
8
|
class Resource
|
7
|
-
|
8
|
-
|
9
|
+
APPLICATION_JSON = 'application/json'
|
10
|
+
|
11
|
+
NO_PARAMS = {}.freeze
|
9
12
|
|
10
13
|
def initialize(client, path)
|
11
14
|
@client = client
|
@@ -20,12 +23,13 @@ module Bunq
|
|
20
23
|
raise Bunq::Timeout
|
21
24
|
end
|
22
25
|
|
23
|
-
|
24
|
-
|
25
|
-
body =
|
26
|
+
def post(payload, skip_verify: false, encrypt: false, custom_headers: {}, &block)
|
27
|
+
custom_headers = JSON.parse(custom_headers.to_json)
|
28
|
+
body = post_body(payload, custom_headers)
|
26
29
|
body, headers = client.encryptor.encrypt(body) if encrypt
|
30
|
+
headers = headers.to_h.merge(custom_headers)
|
27
31
|
|
28
|
-
headers = bunq_request_headers('POST', NO_PARAMS, body, headers
|
32
|
+
headers = bunq_request_headers('POST', NO_PARAMS, body, headers)
|
29
33
|
|
30
34
|
resource.post(body, headers) do |response, request, result|
|
31
35
|
if skip_verify
|
@@ -74,40 +78,43 @@ module Bunq
|
|
74
78
|
x[:user] = client.configuration.sandbox_user
|
75
79
|
x[:password] = client.configuration.sandbox_password
|
76
80
|
end
|
77
|
-
end
|
81
|
+
end,
|
78
82
|
)
|
79
83
|
end
|
80
84
|
|
81
85
|
def bunq_request_headers(verb, params, payload = nil, headers = {})
|
82
|
-
headers[
|
86
|
+
headers[Bunq::Header::CLIENT_REQUEST_ID] = SecureRandom.uuid
|
83
87
|
|
84
88
|
unless @path.end_with?('/installation') && verb == 'POST'
|
85
|
-
headers[
|
89
|
+
headers[Bunq::Header::CLIENT_SIGNATURE] = sign_request(verb, params, headers, payload)
|
86
90
|
end
|
87
91
|
|
88
92
|
headers
|
89
93
|
end
|
90
94
|
|
91
|
-
def sign_request(
|
95
|
+
def sign_request(_verb, _params, _headers, payload = nil)
|
92
96
|
client.signature.create(payload)
|
93
97
|
end
|
94
98
|
|
95
99
|
def encode_params(path, params)
|
96
100
|
return path if params.empty?
|
101
|
+
|
97
102
|
"#{path}?#{URI.escape(params.collect { |k, v| "#{k}=#{v}" }.join('&'))}"
|
98
103
|
end
|
99
104
|
|
100
105
|
def verify_and_handle_response(response, request, result, &block)
|
101
|
-
|
102
|
-
client.signature.verify!(response) unless client.configuration.disable_response_signature_verification
|
106
|
+
client.signature.verify!(response) if verify_response_signature?(response)
|
103
107
|
handle_response(response, request, result, &block)
|
104
108
|
end
|
105
109
|
|
106
|
-
def
|
107
|
-
|
110
|
+
def verify_response_signature?(response)
|
111
|
+
return false if client.configuration.disable_response_signature_verification
|
112
|
+
return false if response.code == 491
|
113
|
+
|
114
|
+
(100..499).include?(response.code)
|
108
115
|
end
|
109
116
|
|
110
|
-
def handle_response(response, _request, _result
|
117
|
+
def handle_response(response, _request, _result)
|
111
118
|
if response.code == 200 || response.code == 201
|
112
119
|
if block_given?
|
113
120
|
yield(response)
|
@@ -116,13 +123,24 @@ module Bunq
|
|
116
123
|
end
|
117
124
|
elsif (response.code == 409 && Bunq.configuration.sandbox) || response.code == 429
|
118
125
|
fail TooManyRequestsResponse.new(code: response.code, headers: response.raw_headers, body: response.body)
|
119
|
-
elsif response.code
|
126
|
+
elsif [401, 403].include?(response.code)
|
120
127
|
fail UnauthorisedResponse.new(code: response.code, headers: response.raw_headers, body: response.body)
|
121
128
|
elsif response.code == 404
|
122
129
|
fail ResourceNotFound.new(code: response.code, headers: response.raw_headers, body: response.body)
|
130
|
+
elsif [491, 503].include?(response.code)
|
131
|
+
fail MaintenanceResponse.new(code: response.code, headers: response.raw_headers, body: response.body)
|
123
132
|
else
|
124
133
|
fail UnexpectedResponse.new(code: response.code, headers: response.raw_headers, body: response.body)
|
125
134
|
end
|
126
135
|
end
|
136
|
+
|
137
|
+
def post_body(payload, custom_headers)
|
138
|
+
if custom_headers.key?(Bunq::Header::CONTENT_TYPE) &&
|
139
|
+
custom_headers[Bunq::Header::CONTENT_TYPE] != APPLICATION_JSON
|
140
|
+
payload
|
141
|
+
else
|
142
|
+
JSON.generate(payload)
|
143
|
+
end
|
144
|
+
end
|
127
145
|
end
|
128
146
|
end
|
data/lib/bunq/session_servers.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Bunq
|
2
4
|
##
|
3
5
|
# https://doc.bunq.com/api/1/call/session-server
|
4
6
|
class SessionServers
|
5
7
|
def initialize(client)
|
6
|
-
@resource = Bunq::Resource.new(client,
|
8
|
+
@resource = Bunq::Resource.new(client, '/v1/session-server')
|
7
9
|
@api_key = client.configuration.api_key
|
8
10
|
end
|
9
11
|
|
@@ -11,7 +13,8 @@ module Bunq
|
|
11
13
|
# https://doc.bunq.com/api/1/call/session-server/method/post
|
12
14
|
def create
|
13
15
|
fail 'Cannot create session, please add the api_key to your configuration' unless @api_key
|
14
|
-
|
16
|
+
|
17
|
+
@resource.post({secret: @api_key})['Response']
|
15
18
|
end
|
16
19
|
end
|
17
20
|
end
|
data/lib/bunq/signature.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative 'errors'
|
2
4
|
|
3
5
|
module Bunq
|
@@ -7,8 +9,8 @@ module Bunq
|
|
7
9
|
BUNQ_SERVER_SIGNATURE_RESPONSE_HEADER = 'X-Bunq-Server-Signature'.downcase
|
8
10
|
|
9
11
|
def initialize(private_key, server_public_key)
|
10
|
-
fail ArgumentError
|
11
|
-
fail ArgumentError
|
12
|
+
fail ArgumentError, 'private_key is mandatory' unless private_key
|
13
|
+
fail ArgumentError, 'server_public_key is mandatory' unless server_public_key
|
12
14
|
|
13
15
|
@private_key = OpenSSL::PKey::RSA.new(private_key)
|
14
16
|
@server_public_key = OpenSSL::PKey::RSA.new(server_public_key)
|
@@ -34,8 +36,8 @@ module Bunq
|
|
34
36
|
end
|
35
37
|
|
36
38
|
signature = Base64.strict_decode64(signature_headers_value.first)
|
37
|
-
|
38
|
-
fail
|
39
|
+
if !verify_modern(signature, response) && !verify_legacy(signature, response)
|
40
|
+
fail InvalidResponseSignature.new(code: response.code, headers: response.raw_headers, body: response.body)
|
39
41
|
end
|
40
42
|
end
|
41
43
|
|
@@ -48,12 +50,33 @@ module Bunq
|
|
48
50
|
end
|
49
51
|
|
50
52
|
def verifiable_header?(header_name, _)
|
51
|
-
|
52
|
-
|
53
|
+
the_header_name = header_name.to_s.downcase
|
54
|
+
the_header_name.start_with?(BUNQ_HEADER_PREFIX) && the_header_name != BUNQ_SERVER_SIGNATURE_RESPONSE_HEADER
|
55
|
+
end
|
56
|
+
|
57
|
+
def skip_signature_check(response_code)
|
58
|
+
(Bunq.configuration.sandbox && response_code == 409) || response_code == 429
|
59
|
+
end
|
60
|
+
|
61
|
+
def verify_legacy(signature, response)
|
62
|
+
sorted_bunq_headers = response
|
63
|
+
.raw_headers
|
64
|
+
.select(&method(:verifiable_header?))
|
65
|
+
.sort
|
66
|
+
.to_h
|
67
|
+
.map do |k, v|
|
68
|
+
"#{k.to_s.split('-').map(&:capitalize).join('-')}: #{v.first}"
|
69
|
+
end
|
70
|
+
|
71
|
+
verify(signature, %(#{response.code}\n#{sorted_bunq_headers.join("\n")}\n\n#{response.body}))
|
72
|
+
end
|
73
|
+
|
74
|
+
def verify_modern(signature, response)
|
75
|
+
verify(signature, response.body)
|
53
76
|
end
|
54
77
|
|
55
|
-
def
|
56
|
-
(
|
78
|
+
def verify(signature, data)
|
79
|
+
server_public_key.verify(digest, signature, data)
|
57
80
|
end
|
58
81
|
end
|
59
82
|
end
|