alula-ruby 1.7.0 → 1.8.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
2
  SHA256:
3
- metadata.gz: 4ba30ce4a88b334fd10633b735f401d59b041cfdf2298a1298b5136e283d4b1d
4
- data.tar.gz: '09475f21199c2182b86e3a775b88ed1df2f6617566bb2e980cd0d109d6536630'
3
+ metadata.gz: 4c9520f289b6a7b5175a8a32b3a1934d6f1ecae534f4054cfa043533d7dd6572
4
+ data.tar.gz: 11bf384b95594822f1a8f4c93c71f294409e78e12d63bbad4f700f3060dd222e
5
5
  SHA512:
6
- metadata.gz: 7266449db3159f70c5a4d77ae25ae59355b7d4dc5e34d8cae838b10292b51c592da29ef192bd363b230f8a65d34195b30d1bfe617db3c25e6595a36f0d133e5b
7
- data.tar.gz: eca19d6a1412ba2b51c4e4ba6514b47fd05fff1445b8fd3f6a9868eca43d0148d3c5498fd076eeb8230adbd4cbe5a31922eff061466c068f8eab5602e4245b6b
6
+ metadata.gz: 68f0c65cda2ed0d8c83e7ac321057f986e39ccadb89df5c7c58a37b1490b46ad3158b776370f87dc8cc6c8f1e68a068f88c8f44ad4d8c5a6581344472ca84021
7
+ data.tar.gz: 9608f45a747a310a1f13ef6341f458ac37b6ed0ba4a5ad30e485532d7424875dd2991562f9b6402152191167fc0a8eccfc0d092b8a84cb3ece489af73d9e1581
data/VERSION.md CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  | Version | Date | Description |
4
4
  | ------- | --------- | --------------------------------------------------------------------------- |
5
+ | v1.8.0 | 2024-06-19 | Use Video API endpoints instead of core API passthrough |
5
6
  | v1.7.0 | 2024-06-13 | Add DELETE, PATCH/POST, unregister capabilities for cameras |
6
7
  | v1.6.0 | 2024-01-26 | Add user language attribute |
7
8
  | v1.5.0 | 2024-01-09 | Add more known errors that use a different format |
@@ -13,6 +13,7 @@ module Alula
13
13
  attributes: as_patchable_json
14
14
  }
15
15
  }
16
+ payload[:data].delete(:id) if video_request?(resource_url)
16
17
 
17
18
  response = Alula::Client.request(:patch, resource_url, payload, {})
18
19
 
@@ -79,6 +80,10 @@ module Alula
79
80
  raise Alula::UnknownError, "Unknown HTTP response code, aborting. Code: #{response.http_status}"
80
81
  end
81
82
  end
83
+
84
+ def video_request?(resource_path)
85
+ resource_path.match(%r{^/video})
86
+ end
82
87
  end
83
88
  end
84
89
  end
data/lib/alula/client.rb CHANGED
@@ -7,7 +7,7 @@ module Alula
7
7
  class << self
8
8
  DEFAULT_CUSTOM_OPTIONS = {
9
9
  omitRelationships: true
10
- }
10
+ }.freeze
11
11
 
12
12
  def config
13
13
  @_config ||= Alula::ClientConfiguration.new
@@ -18,13 +18,13 @@ module Alula
18
18
  end
19
19
 
20
20
  def request(http_method, resource_path, filters = {}, opts = {})
21
- unless resource_path.match(%r{^/public/v1})
22
- validate_request!(http_method, resource_path)
23
- end
24
-
21
+ validate_request!(http_method, resource_path) unless resource_path.match(%r{^/public/v1})
25
22
  request_opts = build_options(http_method, resource_path, filters, opts)
23
+ api_url = video_request?(resource_path) ? config.video_api_url : config.api_url
24
+ request_resource_path = video_request?(resource_path) ? resource_path.gsub(%r{^/video}, '') : resource_path
25
+
26
26
  # TODO: Handle network failures
27
- response = make_request(http_method, config.api_url + resource_path, request_opts)
27
+ response = make_request(http_method, api_url + request_resource_path, request_opts)
28
28
 
29
29
  begin
30
30
  resp = AlulaResponse.from_httparty_response(response)
@@ -41,7 +41,11 @@ module Alula
41
41
  def validate_request!(http_method, resource_path)
42
42
  config.ensure_api_key_set
43
43
  config.ensure_api_url_set
44
- config.ensure_customer_id_set if resource_path.match(%r{^/video})
44
+ if video_request?(resource_path)
45
+ config.ensure_video_api_url_set
46
+ config.ensure_video_api_key_set
47
+ config.ensure_customer_id_set
48
+ end
45
49
  ensure_method_allowable(http_method)
46
50
  config.ensure_role_set if %i[patch post put].include? http_method
47
51
  end
@@ -51,9 +55,13 @@ module Alula
51
55
  end
52
56
 
53
57
  def ensure_method_allowable(http_method)
54
- unless %i[get post put patch delete].include?(http_method)
55
- raise "Unable to send a request with #{http_method} http method"
56
- end
58
+ return if %i[get post put patch delete].include?(http_method)
59
+
60
+ raise "Unable to send a request with #{http_method} http method"
61
+ end
62
+
63
+ def video_request?(resource_path)
64
+ resource_path.match(%r{^/video})
57
65
  end
58
66
 
59
67
  def build_options(http_method, resource_path, filters = {}, opts = {})
@@ -63,6 +71,7 @@ module Alula
63
71
  'User-Agent': "#{Alula::Client.config.user_agent || 'No Agent Set'}/alula-ruby v#{Alula::VERSION}"
64
72
  }
65
73
  }.merge(opts)
74
+ handle_video_request(request_opts) if video_request?(resource_path) # Add token for all video requests
66
75
 
67
76
  request_opts[:headers]['Content-Type'] = 'application/json' unless opts[:multipart]
68
77
  case http_method
@@ -70,11 +79,7 @@ module Alula
70
79
  :post
71
80
  request_opts[:body] = opts[:multipart] ? filters : JSON.generate(filters)
72
81
  when :get
73
- if resource_path.match(%r{^/(rest|rpc)})
74
- request_opts = request_opts.merge build_rest_options(filters)
75
- elsif resource_path.match(%r{^/video})
76
- add_customer_id(request_opts)
77
- end
82
+ request_opts = request_opts.merge build_rest_options(filters) if resource_path.match(%r{^/(rest|rpc)})
78
83
  when :delete
79
84
  # Nothing special
80
85
  end
@@ -111,8 +116,17 @@ module Alula
111
116
  filters
112
117
  end
113
118
 
114
- def add_customer_id(request_opts)
115
- request_opts[:query] = { 'customerId' => config.customer_id }
119
+ def handle_video_request(request_opts)
120
+ if Alula::Client.config.internal
121
+ set_token(request_opts, { internal: true })
122
+ else
123
+ set_token(request_opts, { customerId: Alula::Client.config.customer_id })
124
+ end
125
+ end
126
+
127
+ def set_token(request_opts, payload)
128
+ jwt = TokenExchange.fetch_video_token(payload)
129
+ request_opts[:headers]['token'] = jwt
116
130
  end
117
131
  end
118
132
  end
@@ -1,7 +1,7 @@
1
1
  module Alula
2
2
  class ClientConfiguration
3
- attr_accessor :api_url, :debug, :user_agent
4
- REQUEST_ATTRIBUTES = %i[api_key customer_id]
3
+ attr_accessor :api_url, :video_api_url, :debug, :user_agent
4
+ REQUEST_ATTRIBUTES = %i[api_key video_api_key customer_id internal]
5
5
 
6
6
  # Using RequestStore so we're thread safe even with our non-thread-safe architecture :(
7
7
  REQUEST_ATTRIBUTES.each do |prop|
@@ -42,11 +42,25 @@ module Alula
42
42
  end
43
43
  end
44
44
 
45
+ def ensure_video_api_url_set
46
+ return unless video_api_url.nil?
47
+
48
+ raise Alula::NotConfiguredError,
49
+ 'did you forget to set the Alula::Client.config.video_api_url config option?'
50
+ end
51
+
52
+ def ensure_video_api_key_set
53
+ return if video_api_key
54
+
55
+ raise Alula::NotConfiguredError,
56
+ 'Set your video API access token before making requests to the video API'
57
+ end
58
+
45
59
  def ensure_customer_id_set
46
- unless customer_id
47
- raise Alula::NotConfiguredError,
48
- 'Set your customer_id before making requests to the video API'
49
- end
60
+ return if internal || customer_id
61
+
62
+ raise Alula::NotConfiguredError,
63
+ 'Set your customer_id before making requests to the video API'
50
64
  end
51
65
 
52
66
  def ensure_role_set
data/lib/alula/errors.rb CHANGED
@@ -87,6 +87,8 @@ module Alula
87
87
  NotFoundError.new(message)
88
88
  when 'Device Not Found'
89
89
  NotFoundError.new(message)
90
+ when 'Unable to unregister device'
91
+ NotFoundError.new(message)
90
92
  else
91
93
  Alula.logger.error response
92
94
  raise NotImplementedError, "Unable to derive error for #{response.data['error'].to_json}"
@@ -128,6 +130,8 @@ module Alula
128
130
  NotFoundError.new(message)
129
131
  when 'Device Not Found'
130
132
  NotFoundError.new(message)
133
+ when 'Unable to unregister device'
134
+ NotFoundError.new(message)
131
135
  else
132
136
  Alula.logger.error response
133
137
  raise NotImplementedError, "Unable to derive error for #{response.data['errors'].to_json}"
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+ require 'base64'
3
+ require 'openssl'
4
+
5
+ module Alula
6
+ # Helper class to build JWT tokens
7
+ # Used for internal video API calls or video API calls with a customer ID
8
+ module JwtHelper
9
+ ONE_HOUR = 60 * 60
10
+ class << self
11
+ def build_jwt_token(jwt_payload)
12
+ jwt_secret = Alula::Client.config.video_api_key
13
+
14
+ header = build_jwt_header
15
+
16
+ token_data = add_timestamp_to_payload(jwt_payload)
17
+
18
+ encoded_header = convert_to_base64(JSON.generate(header))
19
+ encoded_data = convert_to_base64(JSON.generate(token_data))
20
+
21
+ token = "#{encoded_header}.#{encoded_data}"
22
+ signature = sign_token(token, jwt_secret)
23
+
24
+ "#{token}.#{signature}"
25
+ end
26
+
27
+ def build_jwt_header
28
+ {
29
+ 'typ' => 'JWT',
30
+ 'alg' => 'HS256'
31
+ }
32
+ end
33
+
34
+ def add_timestamp_to_payload(payload)
35
+ current_timestamp = Time.now.to_i
36
+ payload.merge(
37
+ {
38
+ 'iat' => current_timestamp,
39
+ 'exp' => current_timestamp + ONE_HOUR # expiry time is 60 minutes from time of creation
40
+ }
41
+ )
42
+ end
43
+
44
+ def sign_token(token, secret)
45
+ signature = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), secret, token)
46
+ convert_to_base64(signature)
47
+ end
48
+
49
+ def convert_to_base64(source)
50
+ # Encode in classical base64,
51
+ encoded_source = Base64.strict_encode64(source)
52
+
53
+ # Remove padding equal characters,
54
+ encoded_source = encoded_source.gsub('=', '')
55
+
56
+ # Replace characters according to base64url specifications,
57
+ encoded_source.tr('+/', '-_')
58
+ end
59
+ end
60
+ end
61
+ end
@@ -1,17 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../helpers/jwt_helper'
4
+
1
5
  module Alula
6
+ # Helper class to generate OAuth access tokens (core API) and JWT tokens (VSP API)
2
7
  class TokenExchange
3
- def self.token_for_user(user_id)
4
- url = '/rest/v1/oauth/accesstokens'
5
- payload = { data: { attributes: { userId: user_id } } }
6
- opts = {}
7
-
8
- response = Alula::Client.request(:post, url, payload, opts)
9
-
10
- if response.ok?
11
- ImpersonatedToken.new(response.data['data']['attributes'])
12
- else
13
- error_class = AlulaError.for_response(response)
14
- raise error_class
8
+ class << self
9
+ def token_for_user(user_id)
10
+ url = '/rest/v1/oauth/accesstokens'
11
+ payload = { data: { attributes: { userId: user_id } } }
12
+ opts = {}
13
+
14
+ response = Alula::Client.request(:post, url, payload, opts)
15
+
16
+ if response.ok?
17
+ ImpersonatedToken.new(response.data['data']['attributes'])
18
+ else
19
+ error_class = AlulaError.for_response(response)
20
+ raise error_class
21
+ end
22
+ end
23
+
24
+ def fetch_video_token(payload)
25
+ cache_key = generate_cache_key(payload)
26
+ cached_token, expiry = retrieve_cached_token(cache_key)
27
+
28
+ if cached_token && Time.now.to_i < expiry
29
+ jwt_token = cached_token
30
+ else
31
+ jwt_token = build_jwt_token(payload)
32
+ expiry = Time.now.to_i + JwtHelper::ONE_HOUR # 1 hour expiry
33
+ store_token_in_cache(cache_key, jwt_token, expiry)
34
+ end
35
+
36
+ jwt_token
37
+ end
38
+
39
+ private
40
+
41
+ def build_jwt_token(payload)
42
+ JwtHelper.build_jwt_token(payload)
43
+ end
44
+
45
+ def store_token_in_cache(cache_key, jwt_token, expiry)
46
+ @token_cache ||= {}
47
+ @token_cache[cache_key] = [jwt_token, expiry]
48
+ end
49
+
50
+ def retrieve_cached_token(cache_key)
51
+ @token_cache ||= {}
52
+ @token_cache[cache_key]
53
+ end
54
+
55
+ def generate_cache_key(payload)
56
+ if payload[:internal]
57
+ 'jwt_token_internal'
58
+ elsif payload[:customerId]
59
+ "jwt_token_customer_#{payload[:customerId]}"
60
+ else
61
+ raise ArgumentError, 'Invalid payload'
62
+ end
15
63
  end
16
64
  end
17
65
 
@@ -33,7 +33,7 @@ module Alula
33
33
  api_name :video
34
34
 
35
35
  # Instance method
36
- def resource_url(id = nil)
36
+ def resource_url(id = self.id)
37
37
  "/#{self.class.api_name}/v1/#{self.class.resource_name}/#{id}"
38
38
  end
39
39
 
data/lib/alula/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Alula
4
- VERSION = '1.7.0'
4
+ VERSION = '1.8.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: alula-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.7.0
4
+ version: 1.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Titus Johnson
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-06-17 00:00:00.000000000 Z
11
+ date: 2024-06-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: httparty
@@ -210,6 +210,7 @@ files:
210
210
  - lib/alula/errors.rb
211
211
  - lib/alula/filter_builder.rb
212
212
  - lib/alula/helpers/device_attribute_translations.rb
213
+ - lib/alula/helpers/jwt_helper.rb
213
214
  - lib/alula/list_object.rb
214
215
  - lib/alula/meta.rb
215
216
  - lib/alula/monkey_patches.rb